@modusensus/dsh-mneme 0.11.0 → 0.13.0
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.md +54 -21
- package/docs/CONFIGURATION.md +45 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +744 -26
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3752 -819
- package/lib/config.js +171 -2
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +131 -5
- package/lib/inject.js +192 -19
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +115 -4
- package/lib/peak-hours.js +57 -2
- package/lib/profile-facts.js +356 -0
- package/lib/semantic.js +347 -60
- package/lib/service.js +302 -70
- package/lib/settings.js +117 -14
- package/lib/store.js +830 -55
- package/lib/summarize.js +38 -11
- package/lib/tools.js +173 -25
- package/lib/user-settings.js +175 -23
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +264 -10
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +4 -1
- package/src/api.js +744 -26
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +171 -2
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +131 -5
- package/src/inject.js +192 -19
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +115 -4
- package/src/peak-hours.js +57 -2
- package/src/profile-facts.js +356 -0
- package/src/semantic.js +347 -60
- package/src/service.js +302 -70
- package/src/settings.js +117 -14
- package/src/store.js +830 -55
- package/src/summarize.js +38 -11
- package/src/tools.js +173 -25
- package/src/user-settings.js +175 -23
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +264 -10
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +377 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +908 -138
- package/test/config.test.js +79 -0
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/document.test.js +32 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +38 -7
- package/test/profile-facts.test.js +492 -0
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/session-gate.test.js +1 -1
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +476 -1
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +91 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-tools.test.js +131 -76
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// dsh-mneme/test/wiki-cli-hint.test.js
|
|
2
|
+
// issue #607 的回归锁:提示里那条命令必须真能粘贴就跑。
|
|
3
|
+
//
|
|
4
|
+
// 这条 issue 的病因不是「文案不好看」,是**给了一条跑不起来的命令**:插件装在
|
|
5
|
+
// profile 依赖里,npm 只为全局安装把 shim 放进 PATH,用户照抄 dsh-mneme-choro
|
|
6
|
+
// 必然 command not found。所以真正要锁的不是字符串长相,而是三件事:
|
|
7
|
+
// 1) 推导出的入口在这个仓库里真实存在(下面的 existsSync 与实跑验证);
|
|
8
|
+
// 2) 推导不依赖调用方在第几层目录(src/ 与 lib/ 同层、src/wiki/ 在第二层、
|
|
9
|
+
// bin/ 在第一层)——写死层数会悄悄算错,而算错的表现正是又给一条跑不起来的命令;
|
|
10
|
+
// 3) 找不到入口时返回 null 让调用方降级,而不是硬编一个假路径。
|
|
11
|
+
//
|
|
12
|
+
// 引号部分单独锁:安装路径带空格是常态(Windows 的 C:\Program Files\),
|
|
13
|
+
// POSIX 路径带单引号也必须转义成 '\'',否则「带引号的命令」照样跑不起来。
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
|
|
22
|
+
import { choroCliCommand, choroCliCommandFrom, choroCliEntry } from "../src/wiki/cli-hint.js";
|
|
23
|
+
|
|
24
|
+
const ENTRY_TAIL = join("bin", "dsh-mneme-choro.mjs");
|
|
25
|
+
|
|
26
|
+
test("从包内任意深度都能推出同一个 CLI 入口(不按固定层数)", () => {
|
|
27
|
+
// 四个不同深度、且真实存在于本仓的模块位置;写死 dirname 次数的实现只会有
|
|
28
|
+
// 其中一部分对,这条断言就会红。
|
|
29
|
+
const froms = [
|
|
30
|
+
new URL("../package.json", import.meta.url), // test/ -> 1 层
|
|
31
|
+
new URL("../src/tools.js", import.meta.url), // src/ -> 1 层
|
|
32
|
+
new URL("../src/wiki/t1.js", import.meta.url), // src/wiki/ -> 2 层
|
|
33
|
+
new URL("../bin/dsh-mneme-choro.mjs", import.meta.url) // bin/ -> 0 层
|
|
34
|
+
];
|
|
35
|
+
const entries = froms.map((u) => choroCliEntry(u.href));
|
|
36
|
+
for (const [i, entry] of entries.entries()) {
|
|
37
|
+
assert.equal(typeof entry, "string", "caller #" + i + " must resolve an entry");
|
|
38
|
+
assert.ok(existsSync(entry), "caller #" + i + " resolved a path that does not exist: " + entry);
|
|
39
|
+
assert.ok(entry.endsWith(ENTRY_TAIL), entry);
|
|
40
|
+
}
|
|
41
|
+
// 四条路径必须收敛到同一个入口,否则「同一份代码在不同文件里给出不同命令」
|
|
42
|
+
assert.equal(new Set(entries).size, 1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("推出来的入口是这个包自己的 bin(不是同名外来目录)", () => {
|
|
46
|
+
const entry = choroCliEntry(new URL("../src/wiki/t1.js", import.meta.url).href);
|
|
47
|
+
assert.ok(entry);
|
|
48
|
+
// 包根必须同时有 bin/ 与 package.json —— 这正是推导的两个条件,反过来验一遍
|
|
49
|
+
const pkgRoot = entry.slice(0, entry.length - ENTRY_TAIL.length - 1);
|
|
50
|
+
assert.ok(existsSync(join(pkgRoot, "package.json")), pkgRoot);
|
|
51
|
+
assert.equal(join(pkgRoot, ENTRY_TAIL), entry);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("源码树被裁剪(找不到 bin 或 package.json)时返回 null,不硬编假路径", () => {
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), "mneme-cli-hint-"));
|
|
56
|
+
try {
|
|
57
|
+
// 只有 bin/ 没有 package.json:可能是别人的同名 bin,不能认
|
|
58
|
+
mkdirSync(join(dir, "bin"), { recursive: true });
|
|
59
|
+
writeFileSync(join(dir, "bin", "dsh-mneme-choro.mjs"), "");
|
|
60
|
+
assert.equal(choroCliEntry(pathToFileURL(join(dir, "bin", "dsh-mneme-choro.mjs")).href), null);
|
|
61
|
+
|
|
62
|
+
// 补齐 package.json 就该认出来(证明上一条的红不是「永远返回 null」)
|
|
63
|
+
writeFileSync(join(dir, "package.json"), "{}");
|
|
64
|
+
assert.equal(
|
|
65
|
+
choroCliEntry(pathToFileURL(join(dir, "bin", "dsh-mneme-choro.mjs")).href),
|
|
66
|
+
join(dir, "bin", "dsh-mneme-choro.mjs")
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// 只有 package.json 没有 bin/:`files` 少了 bin 的裁剪树,同样不能认
|
|
70
|
+
const bare = mkdtempSync(join(tmpdir(), "mneme-cli-hint-bare-"));
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(join(bare, "package.json"), "{}");
|
|
73
|
+
mkdirSync(join(bare, "src"), { recursive: true });
|
|
74
|
+
assert.equal(choroCliEntry(pathToFileURL(join(bare, "src", "tools.js")).href), null);
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(bare, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("命令带引号:Windows 双引号,POSIX 单引号(路径含空格与单引号都要能跑)", () => {
|
|
84
|
+
// Windows:安装路径带空格是常态,不加引号那条命令在 cmd 里会被拆成两段
|
|
85
|
+
assert.equal(
|
|
86
|
+
choroCliCommand("C:\\Program Files\\mneme\\bin\\dsh-mneme-choro.mjs", "<repo>", "win32"),
|
|
87
|
+
'node "C:\\Program Files\\mneme\\bin\\dsh-mneme-choro.mjs" <repo> --apply'
|
|
88
|
+
);
|
|
89
|
+
// POSIX:无空格时也加引号(统一形态,便于用户照着改)
|
|
90
|
+
assert.equal(
|
|
91
|
+
choroCliCommand("/opt/mneme/bin/dsh-mneme-choro.mjs", "<repo>", "linux"),
|
|
92
|
+
"node '/opt/mneme/bin/dsh-mneme-choro.mjs' <repo> --apply"
|
|
93
|
+
);
|
|
94
|
+
// POSIX:路径里有单引号,必须转成 '\'',否则 shell 把命令截断在引号处
|
|
95
|
+
assert.equal(
|
|
96
|
+
choroCliCommand("/opt/it's here/bin/dsh-mneme-choro.mjs", "<repo>", "linux"),
|
|
97
|
+
"node '/opt/it'\\''s here/bin/dsh-mneme-choro.mjs' <repo> --apply"
|
|
98
|
+
);
|
|
99
|
+
// 占位符原样保留(面板给 <仓库路径>,工具报错给 <repo>),不被引号逻辑吞掉
|
|
100
|
+
assert.equal(
|
|
101
|
+
choroCliCommandFrom(new URL("../src/tools.js", import.meta.url).href, "<仓库路径>", "win32").endsWith(" <仓库路径> --apply"),
|
|
102
|
+
true
|
|
103
|
+
);
|
|
104
|
+
// 入口缺失时整体返回 null —— 调用方据此降级,绝不输出半条命令
|
|
105
|
+
assert.equal(choroCliCommand(null, "<repo>", "win32"), null);
|
|
106
|
+
assert.equal(choroCliCommandFrom("not a url", "<repo>", "win32"), null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("真的能跑:用推导出的入口执行一次 --help(issue 的验收就是「用户能跑起来」)", () => {
|
|
110
|
+
const entry = choroCliEntry(new URL("../src/wiki/t1.js", import.meta.url).href);
|
|
111
|
+
assert.ok(entry);
|
|
112
|
+
// 用真实入口起一次进程:issue 的验收标准就是「用户照着这条命令能跑起来」,
|
|
113
|
+
const res = spawnSync(process.execPath, [entry, "--help"], { encoding: "utf8", timeout: 30000 });
|
|
114
|
+
assert.equal(res.status, 0, entry + " --help failed: " + (res.stderr || res.stdout));
|
|
115
|
+
assert.match(res.stdout, /--apply/);
|
|
116
|
+
});
|
|
117
|
+
// 用真实入口起一次进程:issue 的验收标准就是「用户照着这条命令能跑起来」,
|
|
118
|
+
// 字符串层面的断言证明不了这件事。
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 盘上缺页如实报 page-missing、检索分节与行号、repo 解析的歧义处理。
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
@@ -46,6 +46,89 @@ function setup() {
|
|
|
46
46
|
return { root, store, q: createChoroQueries({ store }) };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// stub embedder:文本含 "src" → [1,0],否则 [0,1](固定二维,可断言方向与失配)
|
|
50
|
+
function stubEmbedder(model = "stub-1") {
|
|
51
|
+
return {
|
|
52
|
+
model,
|
|
53
|
+
embedSingle: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
54
|
+
embed: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- 收尾档 P37–P39:向量惰性物化与检索 -------------------------------------
|
|
59
|
+
|
|
60
|
+
test("embedPageVectors:首次全嵌,generated_at 没变就跳过,变了重嵌(staleness 两维之一)", async () => {
|
|
61
|
+
const { root, store, q } = setup();
|
|
62
|
+
try {
|
|
63
|
+
const repo = q.resolveRepo(undefined);
|
|
64
|
+
const emb = stubEmbedder();
|
|
65
|
+
const r1 = await q.embedPageVectors(repo.id, emb);
|
|
66
|
+
assert.equal(r1.embedded, 2, "两页全嵌");
|
|
67
|
+
const r2 = await q.embedPageVectors(repo.id, emb);
|
|
68
|
+
assert.equal(r2.embedded, 0);
|
|
69
|
+
assert.equal(r2.skipped, 2, "generated_at 没变 → 全跳过");
|
|
70
|
+
// 页面内容变了(generated_at 推进)→ 只重嵌变的那页
|
|
71
|
+
const pagePath = join(root, ".chorography", "modules", "src.md");
|
|
72
|
+
const before = readFileSync(pagePath, "utf8");
|
|
73
|
+
writeFileSync(pagePath, `${before}<!-- CHORO:HUMAN:START -->\n人工补充\n<!-- CHORO:HUMAN:END -->\n`, "utf8");
|
|
74
|
+
store.db.prepare(
|
|
75
|
+
"UPDATE choro_pages SET generated_at = ? WHERE generated_at IS NOT NULL AND id IN (SELECT p.id FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id WHERE m.slug = 'src')"
|
|
76
|
+
).run("2026-11-06T00:00:00.000Z");
|
|
77
|
+
const r3 = await q.embedPageVectors(repo.id, emb);
|
|
78
|
+
assert.equal(r3.embedded, 1, "只有 src 页重嵌");
|
|
79
|
+
assert.equal(r3.stale, 1, "旧向量按 stale 记账");
|
|
80
|
+
} finally {
|
|
81
|
+
store.close();
|
|
82
|
+
rmTempBestEffort(root);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("dim 失配(换 embedder 模型)→ 惰性全量重嵌(P38:不锁死,代价只是几十页)", async () => {
|
|
87
|
+
const { root, store, q } = setup();
|
|
88
|
+
try {
|
|
89
|
+
const repo = q.resolveRepo(undefined);
|
|
90
|
+
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
91
|
+
// 换模型:维度从 2 → 3。expectDim 来自查询向量(路由先嵌查询再物化)
|
|
92
|
+
const emb3 = {
|
|
93
|
+
model: "stub-2",
|
|
94
|
+
embedSingle: async () => [1, 0, 0],
|
|
95
|
+
embed: async () => [1, 0, 0],
|
|
96
|
+
};
|
|
97
|
+
const r = await q.embedPageVectors(repo.id, emb3, { expectDim: 3 });
|
|
98
|
+
assert.equal(r.embedded, 2, "维度变了 → 全部重嵌(fresh 检查被 dimDrift 压过)");
|
|
99
|
+
assert.equal(r.dimMismatch, true, "失配被记账");
|
|
100
|
+
const rows = store.getChoroPageVectors(repo.id);
|
|
101
|
+
assert.ok(rows.every((row) => row.dim === 3 && row.model === "stub-2"), "旧维度行不复存");
|
|
102
|
+
// 不给 expectDim(无查询场景)→ fresh 行照旧跳过
|
|
103
|
+
const r2 = await q.embedPageVectors(repo.id, emb3);
|
|
104
|
+
assert.equal(r2.embedded, 0);
|
|
105
|
+
assert.equal(r2.skipped, 2);
|
|
106
|
+
} finally {
|
|
107
|
+
store.close();
|
|
108
|
+
rmTempBestEffort(root);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("searchSemantic:cosine 排序 + dim 失配行被过滤 + embedder 缺席返回 null", async () => {
|
|
113
|
+
const { root, store, q } = setup();
|
|
114
|
+
try {
|
|
115
|
+
const repo = q.resolveRepo(undefined);
|
|
116
|
+
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
117
|
+
const hits = await q.searchSemantic(repo.id, stubEmbedder("stub-1"), "src 相关的页");
|
|
118
|
+
assert.ok(Array.isArray(hits) && hits.length >= 1);
|
|
119
|
+
assert.equal(hits[0].slug, "src", "查询向量 [1,0] 与 src 页同向 → 排前");
|
|
120
|
+
// 查询向量三维:与所有行 dim 失配 → 无可比行,返回空数组而不是抛
|
|
121
|
+
const dim3 = { embedSingle: async () => [1, 0, 0], embed: async () => [1, 0, 0] };
|
|
122
|
+
const empty = await q.searchSemantic(repo.id, dim3, "x");
|
|
123
|
+
assert.deepEqual(empty, []);
|
|
124
|
+
assert.equal(await q.searchSemantic(repo.id, null, "x"), null);
|
|
125
|
+
assert.deepEqual(await q.searchSemantic(repo.id, stubEmbedder(), " "), [], "空查询不嵌入、返回空数组");
|
|
126
|
+
} finally {
|
|
127
|
+
store.close();
|
|
128
|
+
rmTempBestEffort(root);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
49
132
|
test("listRepos / resolveRepo:单仓自动收敛,多仓歧义要显式给键", () => {
|
|
50
133
|
const { root, store, q } = setup();
|
|
51
134
|
try {
|
|
@@ -82,6 +165,13 @@ test("moduleMap:模块、页面状态与跨模块边同源返回", () => {
|
|
|
82
165
|
assert.deepEqual(slugs, ["root", "src"]);
|
|
83
166
|
const src = modules.find((m) => m.slug === "src");
|
|
84
167
|
assert.equal(src.page_status, "skeleton");
|
|
168
|
+
// 面板从地志页导入知识点时读的是 mod.relPath(契约 §3.6);SQL 行里只有
|
|
169
|
+
// page_rel_path,缺这层映射会让「选完模块点导入」永远提示请先选模块(#607)。
|
|
170
|
+
// 同时锁住原 snake_case 字段还在——choro_module_map 的 render 与 CLI 都在用。
|
|
171
|
+
assert.equal(typeof src.relPath, "string", "relPath 必须与 page_rel_path 同值");
|
|
172
|
+
assert.equal(src.relPath, src.page_rel_path);
|
|
173
|
+
assert.ok(src.relPath.endsWith(".md"), "页相对路径是磁盘上真实存在的 .md");
|
|
174
|
+
assert.equal(typeof src.file_count, "number", "file_count 原字段必须保留(choro_module_map 的 render 在吃)");
|
|
85
175
|
// fixture 的引用全在 src 模块内部(a→b→b2),left-pad 是外部依赖,root 只有
|
|
86
176
|
// package.json 没有代码——跨模块边应为空集。跨模块聚erge本身由 wiki-store 的
|
|
87
177
|
// 整仓替换用例与 t0 的 moduleEdges 排序覆盖。
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// T1 面板后台任务状态机的回归锁(契约 §3.2)。
|
|
2
|
+
//
|
|
3
|
+
// **绝不真起进程**:T1 一次要花模型额度,测试里注入假 spawn 就够——这一层要证明的
|
|
4
|
+
// 是「记账与状态迁移对不对」,不是「模型跑得通不通」(那是 CLI 的 e2e 地盘)。
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { createT1Jobs } from "../src/wiki/t1-jobs.js";
|
|
9
|
+
|
|
10
|
+
// 假子进程:stdout/stderr 是 EventEmitter,close/error 也走事件——形状贴着真
|
|
11
|
+
// ChildProcess,但一个字节都不会真跑出去。
|
|
12
|
+
function fakeSpawn() {
|
|
13
|
+
const calls = [];
|
|
14
|
+
const spawn = (cmd, args, opts) => {
|
|
15
|
+
const child = new EventEmitter();
|
|
16
|
+
child.stdout = new EventEmitter();
|
|
17
|
+
child.stderr = new EventEmitter();
|
|
18
|
+
child.stdout.setEncoding = () => {};
|
|
19
|
+
child.stderr.setEncoding = () => {};
|
|
20
|
+
child.killed = false;
|
|
21
|
+
child.kill = () => { child.killed = true; };
|
|
22
|
+
child.cmd = cmd;
|
|
23
|
+
child.args = args;
|
|
24
|
+
child.opts = opts;
|
|
25
|
+
calls.push(child);
|
|
26
|
+
return child;
|
|
27
|
+
};
|
|
28
|
+
return { spawn, calls };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeJobs(overrides = {}) {
|
|
32
|
+
const fake = fakeSpawn();
|
|
33
|
+
const jobs = createT1Jobs({
|
|
34
|
+
spawn: fake.spawn,
|
|
35
|
+
nodePath: "node",
|
|
36
|
+
scriptPath: "/pkg/bin/dsh-mneme-choro.mjs",
|
|
37
|
+
dbPath: "/mem/memory.db",
|
|
38
|
+
now: (() => { let i = 0; return () => `T${++i}`; })(),
|
|
39
|
+
...overrides
|
|
40
|
+
});
|
|
41
|
+
return { jobs, calls: fake.calls };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const repo = { id: 1, name: "r", root_path: "/repo" };
|
|
45
|
+
|
|
46
|
+
test("start:起的是 CLI 的 --t1 --apply --json(面板必须带 apply,否则 CLI 只列候选不写)", () => {
|
|
47
|
+
const { jobs, calls } = makeJobs();
|
|
48
|
+
const r = jobs.start({ repo, module: "src", retryFailed: true });
|
|
49
|
+
assert.equal(r.started, true);
|
|
50
|
+
assert.ok(r.jobId);
|
|
51
|
+
assert.equal(calls.length, 1);
|
|
52
|
+
const [cmd, args, opts] = [calls[0].cmd, calls[0].args, calls[0].opts];
|
|
53
|
+
assert.equal(cmd, "node");
|
|
54
|
+
assert.deepEqual(args.slice(0, 5), ["/pkg/bin/dsh-mneme-choro.mjs", "/repo", "--t1", "--apply", "--json"]);
|
|
55
|
+
assert.ok(args.includes("--module") && args.includes("src"));
|
|
56
|
+
assert.ok(args.includes("--retry-failed"));
|
|
57
|
+
assert.ok(args.includes("--db") && args.includes("/mem/memory.db"));
|
|
58
|
+
// Electron 宿主里不设这个变量会去开窗口而不是跑脚本——这条是踩过的坑。
|
|
59
|
+
assert.equal(opts.env.ELECTRON_RUN_AS_NODE, "1");
|
|
60
|
+
assert.deepEqual(opts.stdio, ["ignore", "pipe", "pipe"]);
|
|
61
|
+
jobs.dispose();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("同一仓库并发 → job-running 且带回在跑的 jobId(面板据此继续轮询而不是重起)", () => {
|
|
65
|
+
const { jobs } = makeJobs();
|
|
66
|
+
const first = jobs.start({ repo });
|
|
67
|
+
const second = jobs.start({ repo });
|
|
68
|
+
assert.equal(second.started, false);
|
|
69
|
+
assert.equal(second.code, "job-running");
|
|
70
|
+
assert.equal(second.jobId, first.jobId);
|
|
71
|
+
|
|
72
|
+
// 不同仓库互不影响:并发闸是按仓库的,不是全局的。
|
|
73
|
+
const other = jobs.start({ repo: { id: 2, name: "o", root_path: "/repo2" } });
|
|
74
|
+
assert.equal(other.started, true);
|
|
75
|
+
jobs.dispose();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("stdout 逐行进 logTail(封顶 200 行),chunk 切在一行中间也不丢字", async () => {
|
|
79
|
+
const { jobs, calls } = makeJobs();
|
|
80
|
+
const { jobId } = jobs.start({ repo });
|
|
81
|
+
const child = calls[0];
|
|
82
|
+
// 故意把一行拆成两个 chunk:不做余量拼接的实现会把它切成两条假日志。
|
|
83
|
+
child.stdout.emit("data", "第一行\n第二");
|
|
84
|
+
child.stdout.emit("data", "行\n");
|
|
85
|
+
let view = jobs.status(jobId);
|
|
86
|
+
assert.deepEqual(view.logTail, ["第一行", "第二行"]);
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < 300; i += 1) child.stdout.emit("data", `l${i}\n`);
|
|
89
|
+
view = jobs.status(jobId);
|
|
90
|
+
assert.equal(view.logTail.length, 200, "ring buffer 封顶:长任务 stdout 不封顶就是内存泄漏");
|
|
91
|
+
assert.equal(view.logTail[199], "l299", "留下的是最新的尾巴(面板要看的是当前在干嘛)");
|
|
92
|
+
jobs.dispose();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("退出码 0 → done 且从回执读出进度;非 0 → failed 且带 stderr 尾巴", () => {
|
|
96
|
+
const { jobs, calls } = makeJobs();
|
|
97
|
+
const ok = jobs.start({ repo });
|
|
98
|
+
// CLI --json 输出的是多行 JSON(不是 NDJSON):按行收日志、按全文解析回执,两条路
|
|
99
|
+
// 各走各的,才不会互相牺牲。
|
|
100
|
+
calls[0].stdout.emit("data", JSON.stringify({ dryRun: false, enriched: [{ slug: "a" }], failed: [{ slug: "b" }] }, null, 2));
|
|
101
|
+
calls[0].emit("close", 0);
|
|
102
|
+
const done = jobs.status(ok.jobId);
|
|
103
|
+
assert.equal(done.state, "done");
|
|
104
|
+
assert.deepEqual(done.modules, { total: 2, done: 1, failed: 1 });
|
|
105
|
+
assert.ok(done.finishedAt);
|
|
106
|
+
|
|
107
|
+
const bad = jobs.start({ repo: { id: 2, name: "o", root_path: "/repo2" } });
|
|
108
|
+
calls[1].stderr.emit("data", "拒绝运行(not-registered):仓库没登记\n");
|
|
109
|
+
calls[1].emit("close", 1);
|
|
110
|
+
const failed = jobs.status(bad.jobId);
|
|
111
|
+
assert.equal(failed.state, "failed");
|
|
112
|
+
// 只报 exit code 等于让面板说「失败了,不知道为啥」——stderr 尾巴才是人话。
|
|
113
|
+
assert.match(failed.error, /not-registered/);
|
|
114
|
+
jobs.dispose();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("未知 jobId → null;dispose 杀掉在跑的进程并清账", () => {
|
|
118
|
+
const { jobs, calls } = makeJobs();
|
|
119
|
+
assert.equal(jobs.status("nope"), null);
|
|
120
|
+
const { jobId } = jobs.start({ repo });
|
|
121
|
+
assert.equal(jobs.status(jobId).state, "running");
|
|
122
|
+
jobs.dispose();
|
|
123
|
+
assert.equal(calls[0].killed, true, "宿主卸载时要带走子进程,不留孤儿");
|
|
124
|
+
assert.equal(jobs.status(jobId), null);
|
|
125
|
+
});
|
package/test/wiki-tools.test.js
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
|
-
//
|
|
2
|
-
// 1. 默认配置下三个 choro_* 工具根本不注册(模型连名字都看不到,与 memory_organize
|
|
3
|
-
// 同款反向语义)——tools.test.js 的 11 工具清单锁管默认面,这里管开启面;
|
|
4
|
-
// 2. chorographyEnabled 开 + queries 装配 → 三工具在场;
|
|
5
|
-
// 3. 开了开关但 queries 缺装配(null)→ 同样不在场(「开了没装配」与「没开」
|
|
6
|
-
// 对模型都不可见,可区分性由面板路由的 409/503 负责);
|
|
7
|
-
// 4. execute 的错误形状:多仓歧义带回可选清单、未知 slug 报 page-not-found。
|
|
1
|
+
// #579:从宿主的工具执行入口验证,绕过 schema 校验直调 execute 会漏掉原故障。
|
|
8
2
|
import test from "node:test";
|
|
9
3
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
11
5
|
import { tmpdir } from "node:os";
|
|
12
6
|
import { join } from "node:path";
|
|
13
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
9
|
+
import SystemPrompt from "@deepseek-ai/dsh-system-prompt";
|
|
10
|
+
import ToolRuntime from "@deepseek-ai/dsh-tools";
|
|
14
11
|
import { createStore } from "../src/store.js";
|
|
15
12
|
import { createService } from "../src/service.js";
|
|
16
13
|
import { createTools } from "../src/tools.js";
|
|
17
14
|
import { createChoroQueries } from "../src/wiki/queries.js";
|
|
18
15
|
import { runChoroT0 } from "../src/wiki/t0.js";
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
const choroArgs = {
|
|
18
|
+
choro_module_map: {},
|
|
19
|
+
choro_read_page: { slug: "src" },
|
|
20
|
+
choro_search: { query: "b.js" }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function setup(t, config = {}, withChoro = false) {
|
|
21
24
|
const store = createStore(":memory:");
|
|
25
|
+
t.after(() => store.close());
|
|
26
|
+
const ctx = new Context();
|
|
27
|
+
const prompt = await ctx.plugin(SystemPrompt);
|
|
28
|
+
const runtime = await ctx.plugin(ToolRuntime);
|
|
29
|
+
t.after(() => { runtime.dispose(); prompt.dispose(); });
|
|
30
|
+
const tools = ctx.get("tools");
|
|
22
31
|
const service = createService({ store, mirror: null, config: {} });
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return () => {};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
createTools(ctx, service, config, null, null, choro);
|
|
33
|
-
return { registered, store };
|
|
32
|
+
createTools({ tools }, service, config, null, null, withChoro ? createChoroQueries({ store }) : null);
|
|
33
|
+
const call = (name, args = {}) => tools.execute({
|
|
34
|
+
name, callId: name, arguments: args, signal: new AbortController().signal
|
|
35
|
+
});
|
|
36
|
+
return { tools, store, call };
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
function makeRepo(store) {
|
|
39
|
+
function makeRepo(t, store, name = "toolfix") {
|
|
37
40
|
const root = mkdtempSync(join(tmpdir(), "choro-tools-"));
|
|
38
|
-
|
|
41
|
+
t.after(() => rmTempBestEffort(root));
|
|
42
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name, scripts: { test: "node --test" } }, null, 2) + "\n", "utf8");
|
|
39
43
|
mkdirSync(join(root, "src"), { recursive: true });
|
|
40
44
|
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nexport const a = () => b();\n', "utf8");
|
|
41
45
|
writeFileSync(join(root, "src", "b.js"), "export const b = () => 1;\n", "utf8");
|
|
@@ -55,67 +59,118 @@ function rmTempBestEffort(dir) {
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
function expectToolError(result, pattern) {
|
|
63
|
+
const message = result.content[0]?.text ?? "";
|
|
64
|
+
assert.equal(result.isError, true, message);
|
|
65
|
+
assert.match(message, pattern);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("默认配置下三个 choro 工具不注册(模型看不到就不会调)", async (t) => {
|
|
69
|
+
const { tools } = await setup(t, {}, true);
|
|
70
|
+
for (const name of Object.keys(choroArgs)) {
|
|
71
|
+
assert.ok(!tools.get(name), `${name} 默认缺席`);
|
|
63
72
|
}
|
|
64
73
|
});
|
|
65
74
|
|
|
66
|
-
test("
|
|
67
|
-
const {
|
|
68
|
-
const
|
|
69
|
-
assert.ok(!names.includes("choro_search"));
|
|
75
|
+
test("开关开但 queries 缺装配 → 同样缺席(可区分性归路由层)", async (t) => {
|
|
76
|
+
const { tools } = await setup(t, { chorographyEnabled: true });
|
|
77
|
+
for (const name of Object.keys(choroArgs)) assert.ok(!tools.get(name));
|
|
70
78
|
});
|
|
71
79
|
|
|
72
|
-
test("
|
|
73
|
-
const store =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
rmTempBestEffort(root);
|
|
80
|
+
test("开启后三个只读工具的成功结果通过宿主校验", async (t) => {
|
|
81
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
82
|
+
makeRepo(t, store);
|
|
83
|
+
const map = await call("choro_module_map");
|
|
84
|
+
assert.equal(map.isError, false);
|
|
85
|
+
assert.equal(map.value.repo, "toolfix");
|
|
86
|
+
assert.ok(map.value.modules.length >= 2);
|
|
87
|
+
const page = await call("choro_read_page", { slug: "src" });
|
|
88
|
+
assert.equal(page.isError, false);
|
|
89
|
+
assert.equal(page.value.slug, "src");
|
|
90
|
+
assert.match(page.value.content, /## 文件清单/);
|
|
91
|
+
const search = await call("choro_search", { query: "b.js" });
|
|
92
|
+
assert.equal(search.isError, false);
|
|
93
|
+
assert.ok(search.value.hits.length >= 1);
|
|
94
|
+
assert.equal(search.value.unreadablePages, undefined);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("未登记或找不到指定仓库时,三个工具都返回可操作的错误", async (t) => {
|
|
98
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
99
|
+
for (const [name, args] of Object.entries(choroArgs)) {
|
|
100
|
+
expectToolError(await call(name, args), /No repository is registered.*dsh-mneme-choro/);
|
|
101
|
+
}
|
|
102
|
+
makeRepo(t, store);
|
|
103
|
+
const [{ id }] = createChoroQueries({ store }).listRepos();
|
|
104
|
+
const unknownId = String(id + 1);
|
|
105
|
+
for (const [name, args] of Object.entries(choroArgs)) {
|
|
106
|
+
expectToolError(await call(name, { ...args, repo: unknownId }), /Repository "\d+" is not registered/);
|
|
100
107
|
}
|
|
101
108
|
});
|
|
102
109
|
|
|
103
|
-
test("
|
|
104
|
-
const store =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
assert.equal(result.error, "repo-ambiguous");
|
|
113
|
-
assert.equal(result.repos.length, 2);
|
|
114
|
-
const named = await map.execute({ repo: "toolfix" });
|
|
115
|
-
assert.ok(named.modules, "显式给名后解析成功(fixture 同名仓按 id 序取先者)");
|
|
116
|
-
} finally {
|
|
117
|
-
store.close();
|
|
118
|
-
rmTempBestEffort(root1);
|
|
119
|
-
rmTempBestEffort(root2);
|
|
110
|
+
test("多仓歧义提示可选 id,按 id 重试可读到对应仓库", async (t) => {
|
|
111
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
112
|
+
makeRepo(t, store);
|
|
113
|
+
makeRepo(t, store, "other");
|
|
114
|
+
const repos = createChoroQueries({ store }).listRepos();
|
|
115
|
+
for (const [name, args] of Object.entries(choroArgs)) {
|
|
116
|
+
const result = await call(name, args);
|
|
117
|
+
expectToolError(result, /More than one repository is registered/);
|
|
118
|
+
for (const { id, name } of repos) assert.ok(result.content[0].text.includes(`${id} (${name})`));
|
|
120
119
|
}
|
|
120
|
+
const selected = await call("choro_module_map", { repo: String(repos[1].id) });
|
|
121
|
+
assert.equal(selected.isError, false);
|
|
122
|
+
assert.equal(selected.value.repo, "other");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("读页错误区分未知 slug、缺失文件和不可读文件", async (t) => {
|
|
126
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
127
|
+
const root = makeRepo(t, store);
|
|
128
|
+
expectToolError(await call("choro_read_page", { slug: "nope" }), /Page "nope" was not found.*choro_module_map/);
|
|
129
|
+
const path = join(root, ".chorography", "modules", "src.md");
|
|
130
|
+
unlinkSync(path);
|
|
131
|
+
expectToolError(await call("choro_read_page", { slug: "src" }), /Page "src" is missing from disk.*dsh-mneme-choro/);
|
|
132
|
+
mkdirSync(path);
|
|
133
|
+
const unreadable = await call("choro_read_page", { slug: "src" });
|
|
134
|
+
expectToolError(unreadable, /Page "src" could not be read.*permissions/);
|
|
135
|
+
assert.ok(!unreadable.content[0].text.includes(root), "工具错误不暴露仓库绝对路径");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("共享库中本机不可达的仓库仍有模块图,读页和检索报告文件访问失败", async (t) => {
|
|
139
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
140
|
+
const root = makeRepo(t, store);
|
|
141
|
+
const [{ id }] = createChoroQueries({ store }).listRepos();
|
|
142
|
+
store.db.prepare("UPDATE choro_repos SET root_path = ? WHERE id = ?")
|
|
143
|
+
.run(join(root, "unavailable-checkout"), id);
|
|
144
|
+
const repo = String(id);
|
|
145
|
+
|
|
146
|
+
const map = await call("choro_module_map", { repo });
|
|
147
|
+
assert.equal(map.isError, false);
|
|
148
|
+
assert.equal(map.value.repo, "toolfix");
|
|
149
|
+
assert.ok(map.value.modules.some(({ slug }) => slug === "src"));
|
|
150
|
+
expectToolError(await call("choro_read_page", { repo, slug: "src" }), /missing from disk.*local checkout/);
|
|
151
|
+
expectToolError(await call("choro_search", { repo, query: "b.js" }), /Search was incomplete.*src.*local checkout/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("检索报告不可读的页,保留已读到的命中", async (t) => {
|
|
155
|
+
const { call, store } = await setup(t, { chorographyEnabled: true }, true);
|
|
156
|
+
const root = makeRepo(t, store);
|
|
157
|
+
const rootPage = join(root, ".chorography", "modules", "root.md");
|
|
158
|
+
unlinkSync(rootPage);
|
|
159
|
+
const partial = await call("choro_search", { query: "b.js" });
|
|
160
|
+
assert.equal(partial.isError, false);
|
|
161
|
+
assert.ok(partial.value.hits.length >= 1);
|
|
162
|
+
assert.deepEqual(partial.value.unreadablePages, ["root"]);
|
|
163
|
+
assert.match(partial.content[0].text, /Search was incomplete.*root/);
|
|
164
|
+
|
|
165
|
+
const empty = await call("choro_search", { query: "not present", module: "src" });
|
|
166
|
+
assert.equal(empty.isError, false);
|
|
167
|
+
assert.deepEqual(empty.value, { hits: [] });
|
|
168
|
+
assert.equal(empty.content[0].text, "0 hits.");
|
|
169
|
+
|
|
170
|
+
const incomplete = await call("choro_search", { query: "not present" });
|
|
171
|
+
expectToolError(incomplete, /Search was incomplete.*root/);
|
|
172
|
+
const srcPage = join(root, ".chorography", "modules", "src.md");
|
|
173
|
+
unlinkSync(srcPage);
|
|
174
|
+
mkdirSync(srcPage);
|
|
175
|
+
expectToolError(await call("choro_search", { query: "b.js" }), /Search was incomplete.*root.*src/);
|
|
121
176
|
});
|