2026-08-02 21:49:53 +08:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const test = require("node:test");
|
|
|
|
|
const assert = require("node:assert/strict");
|
|
|
|
|
const childProcess = require("node:child_process");
|
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
const os = require("node:os");
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
const {
|
|
|
|
|
HoloLakeAiGateway,
|
|
|
|
|
HoloLakeKnowledgeProvider,
|
|
|
|
|
} = require("./hololake-capabilities");
|
|
|
|
|
|
|
|
|
|
function createBareKnowledgeRepository() {
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hololake-knowledge-"));
|
|
|
|
|
const work = path.join(root, "work");
|
|
|
|
|
const bare = path.join(root, "knowledge.git");
|
|
|
|
|
fs.mkdirSync(work);
|
|
|
|
|
childProcess.execFileSync("git", ["init", "-b", "main"], { cwd: work });
|
|
|
|
|
childProcess.execFileSync("git", ["config", "user.email", "test@example.invalid"], { cwd: work });
|
|
|
|
|
childProcess.execFileSync("git", ["config", "user.name", "HoloLake Test"], { cwd: work });
|
|
|
|
|
fs.writeFileSync(path.join(work, "INDEX.md"), "# HoloLake\n");
|
|
|
|
|
childProcess.execFileSync("git", ["add", "INDEX.md"], { cwd: work });
|
|
|
|
|
childProcess.execFileSync("git", ["commit", "-m", "knowledge baseline"], { cwd: work });
|
|
|
|
|
childProcess.execFileSync("git", ["clone", "--bare", work, bare]);
|
|
|
|
|
return { root, bare };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("knowledge provider exposes only the fixed main commit and archive", () => {
|
|
|
|
|
const repository = createBareKnowledgeRepository();
|
|
|
|
|
const provider = new HoloLakeKnowledgeProvider({
|
|
|
|
|
repositoryId: "bingshuo/hololake-knowledge-base",
|
|
|
|
|
repositoryPath: repository.bare,
|
|
|
|
|
maxArchiveBytes: 2 * 1024 * 1024,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const manifest = provider.manifest();
|
|
|
|
|
assert.equal(
|
|
|
|
|
manifest.repository,
|
|
|
|
|
"bingshuo/hololake-knowledge-base",
|
|
|
|
|
);
|
|
|
|
|
assert.match(manifest.commit, /^[0-9a-f]{40}$/);
|
|
|
|
|
assert.equal(manifest.ref, "refs/heads/main");
|
|
|
|
|
|
|
|
|
|
const archive = provider.archive(manifest.commit);
|
|
|
|
|
assert.equal(archive.commit, manifest.commit);
|
|
|
|
|
assert.equal(archive.content_type, "application/zip");
|
|
|
|
|
assert.equal(archive.body.subarray(0, 2).toString("ascii"), "PK");
|
|
|
|
|
assert.equal(archive.sha256.length, 64);
|
|
|
|
|
|
|
|
|
|
assert.throws(
|
|
|
|
|
() => provider.archive("0000000000000000000000000000000000000000"),
|
|
|
|
|
/knowledge_commit_not_current/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
test("knowledge provider atomically writes Markdown and returns a verifiable commit receipt", () => {
|
|
|
|
|
const repository = createBareKnowledgeRepository();
|
|
|
|
|
const provider = new HoloLakeKnowledgeProvider({
|
|
|
|
|
repositoryId: "bingshuo/hololake-knowledge-base",
|
|
|
|
|
repositoryPath: repository.bare,
|
|
|
|
|
});
|
|
|
|
|
const base = provider.manifest().commit;
|
|
|
|
|
const content = "# 手机知识页\n\n由 HoloLake Agent 写入。\n";
|
|
|
|
|
|
|
|
|
|
const receipt = provider.writePage({
|
|
|
|
|
path: "个人知识/手机知识页.md",
|
|
|
|
|
content,
|
|
|
|
|
baseCommit: base,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(receipt.schema, "guanghu.hololake-knowledge-write-receipt/v1");
|
|
|
|
|
assert.equal(receipt.repository, "bingshuo/hololake-knowledge-base");
|
|
|
|
|
assert.equal(receipt.path, "个人知识/手机知识页.md");
|
|
|
|
|
assert.equal(receipt.base_commit, base);
|
|
|
|
|
assert.match(receipt.commit, /^[0-9a-f]{40}$/);
|
|
|
|
|
assert.notEqual(receipt.commit, base);
|
|
|
|
|
assert.equal(
|
|
|
|
|
receipt.content_sha256,
|
|
|
|
|
require("node:crypto").createHash("sha256").update(content).digest("hex"),
|
|
|
|
|
);
|
|
|
|
|
assert.equal(provider.manifest().commit, receipt.commit);
|
|
|
|
|
assert.equal(
|
|
|
|
|
childProcess.execFileSync(
|
|
|
|
|
"git",
|
|
|
|
|
[`--git-dir=${repository.bare}`, "show", `${receipt.commit}:个人知识/手机知识页.md`],
|
|
|
|
|
{ encoding: "utf8" },
|
|
|
|
|
),
|
|
|
|
|
content,
|
|
|
|
|
);
|
|
|
|
|
assert.doesNotMatch(JSON.stringify(receipt), /由 HoloLake Agent 写入/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("knowledge provider rejects secret pages, unsafe paths, oversized content, and stale commits", () => {
|
|
|
|
|
const repository = createBareKnowledgeRepository();
|
|
|
|
|
const provider = new HoloLakeKnowledgeProvider({
|
|
|
|
|
repositoryId: "bingshuo/hololake-knowledge-base",
|
|
|
|
|
repositoryPath: repository.bare,
|
|
|
|
|
});
|
|
|
|
|
const base = provider.manifest().commit;
|
|
|
|
|
|
|
|
|
|
for (const invalidPath of [
|
|
|
|
|
"本地密钥/openai.md",
|
|
|
|
|
"../outside.md",
|
|
|
|
|
"notes/plain.txt",
|
|
|
|
|
"/absolute.md",
|
|
|
|
|
"notes//empty.md",
|
|
|
|
|
]) {
|
|
|
|
|
assert.throws(
|
|
|
|
|
() => provider.writePage({
|
|
|
|
|
path: invalidPath,
|
|
|
|
|
content: "# blocked\n",
|
|
|
|
|
baseCommit: base,
|
|
|
|
|
}),
|
|
|
|
|
/knowledge_page_path_invalid|knowledge_secret_page_forbidden/,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
assert.throws(
|
|
|
|
|
() => provider.writePage({
|
|
|
|
|
path: "notes/large.md",
|
|
|
|
|
content: "x".repeat(1024 * 1024 + 1),
|
|
|
|
|
baseCommit: base,
|
|
|
|
|
}),
|
|
|
|
|
/knowledge_page_too_large/,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const first = provider.writePage({
|
|
|
|
|
path: "notes/first.md",
|
|
|
|
|
content: "# first\n",
|
|
|
|
|
baseCommit: base,
|
|
|
|
|
});
|
|
|
|
|
assert.match(first.commit, /^[0-9a-f]{40}$/);
|
|
|
|
|
assert.throws(
|
|
|
|
|
() => provider.writePage({
|
|
|
|
|
path: "notes/stale.md",
|
|
|
|
|
content: "# stale\n",
|
|
|
|
|
baseCommit: base,
|
|
|
|
|
}),
|
|
|
|
|
/knowledge_commit_conflict/,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(provider.manifest().commit, first.commit);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-02 21:49:53 +08:00
|
|
|
test("AI gateway accepts only registered provider models and never leaks the key", async () => {
|
|
|
|
|
const requests = [];
|
|
|
|
|
const gateway = new HoloLakeAiGateway({
|
|
|
|
|
providers: {
|
|
|
|
|
default: {
|
|
|
|
|
baseUrl: "https://models.example.invalid/v1",
|
|
|
|
|
apiKey: "server-secret-key",
|
|
|
|
|
models: ["gpt-test"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
fetchImpl: async (url, options) => {
|
|
|
|
|
requests.push({ url, options });
|
|
|
|
|
return new Response(JSON.stringify({
|
|
|
|
|
id: "response-1",
|
|
|
|
|
choices: [{ message: { role: "assistant", content: "ok" } }],
|
|
|
|
|
usage: { total_tokens: 3 },
|
|
|
|
|
}), {
|
|
|
|
|
status: 200,
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "user", content: "hello" }],
|
|
|
|
|
max_tokens: 32,
|
|
|
|
|
});
|
|
|
|
|
assert.equal(result.ok, true);
|
|
|
|
|
assert.equal(result.response.choices[0].message.content, "ok");
|
|
|
|
|
assert.equal(result.receipt.provider, "default");
|
|
|
|
|
assert.equal(requests.length, 1);
|
|
|
|
|
assert.equal(
|
|
|
|
|
requests[0].options.headers.authorization,
|
|
|
|
|
"Bearer server-secret-key",
|
|
|
|
|
);
|
|
|
|
|
assert.doesNotMatch(JSON.stringify(result), /server-secret-key/);
|
|
|
|
|
const catalog = gateway.catalog();
|
|
|
|
|
assert.deepEqual(catalog.providers[0].models, ["gpt-test"]);
|
|
|
|
|
assert.doesNotMatch(JSON.stringify(catalog), /server-secret-key|models\\.example/);
|
|
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "not-registered",
|
|
|
|
|
messages: [{ role: "user", content: "hello" }],
|
|
|
|
|
}),
|
|
|
|
|
/ai_model_not_registered/,
|
|
|
|
|
);
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "https://attacker.invalid/v1",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "user", content: "hello" }],
|
|
|
|
|
}),
|
|
|
|
|
/ai_provider_not_registered/,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(requests.length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-03 22:27:09 +08:00
|
|
|
test("AI gateway forwards bounded tool contracts and tool receipts without exposing server secrets", async () => {
|
|
|
|
|
const requests = [];
|
|
|
|
|
const gateway = new HoloLakeAiGateway({
|
|
|
|
|
providers: {
|
|
|
|
|
default: {
|
|
|
|
|
baseUrl: "https://models.example.invalid/v1",
|
|
|
|
|
apiKey: "server-secret-key",
|
|
|
|
|
models: ["gpt-test"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
fetchImpl: async (_url, options) => {
|
|
|
|
|
requests.push(JSON.parse(options.body));
|
|
|
|
|
return new Response(JSON.stringify({
|
|
|
|
|
choices: [{ message: { role: "assistant", content: "saved" } }],
|
|
|
|
|
}), { status: 200 });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const result = await gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [
|
|
|
|
|
{ role: "user", content: "save this" },
|
|
|
|
|
{
|
|
|
|
|
role: "assistant",
|
|
|
|
|
content: null,
|
|
|
|
|
tool_calls: [{
|
|
|
|
|
id: "call-1",
|
|
|
|
|
type: "function",
|
|
|
|
|
function: { name: "create_note", arguments: "{\"path\":\"notes/a.md\"}" },
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
{ role: "tool", tool_call_id: "call-1", content: "commit=abc" },
|
|
|
|
|
],
|
|
|
|
|
tools: [{
|
|
|
|
|
type: "function",
|
|
|
|
|
function: {
|
|
|
|
|
name: "create_note",
|
|
|
|
|
description: "Create one Markdown page.",
|
|
|
|
|
parameters: {
|
|
|
|
|
type: "object",
|
|
|
|
|
properties: { path: { type: "string" } },
|
|
|
|
|
required: ["path"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}],
|
|
|
|
|
tool_choice: "auto",
|
|
|
|
|
});
|
|
|
|
|
assert.equal(result.response.choices[0].message.content, "saved");
|
|
|
|
|
assert.equal(requests[0].tools[0].function.name, "create_note");
|
|
|
|
|
assert.equal(requests[0].messages[2].role, "tool");
|
|
|
|
|
assert.equal(requests[0].tool_choice, "auto");
|
|
|
|
|
assert.doesNotMatch(JSON.stringify(result), /server-secret-key/);
|
|
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "tool", tool_call_id: "../bad", content: "x" }],
|
|
|
|
|
}),
|
|
|
|
|
/ai_message_invalid/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-02 21:49:53 +08:00
|
|
|
test("AI gateway bounds message shape, count, text, and requested output", async () => {
|
|
|
|
|
const gateway = new HoloLakeAiGateway({
|
|
|
|
|
providers: {
|
|
|
|
|
default: {
|
|
|
|
|
baseUrl: "https://models.example.invalid/v1",
|
|
|
|
|
apiKey: "server-secret-key",
|
|
|
|
|
models: ["gpt-test"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
fetchImpl: async () => new Response("{}", { status: 200 }),
|
|
|
|
|
maxMessages: 2,
|
|
|
|
|
maxInputCharacters: 10,
|
|
|
|
|
maxOutputTokens: 64,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "user", content: "12345678901" }],
|
|
|
|
|
}),
|
|
|
|
|
/ai_input_too_large/,
|
|
|
|
|
);
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "tool", content: "hello" }],
|
|
|
|
|
}),
|
|
|
|
|
/ai_message_invalid/,
|
|
|
|
|
);
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
() => gateway.execute({
|
|
|
|
|
provider: "default",
|
|
|
|
|
model: "gpt-test",
|
|
|
|
|
messages: [{ role: "user", content: "hello" }],
|
|
|
|
|
max_tokens: 65,
|
|
|
|
|
}),
|
|
|
|
|
/ai_output_limit_exceeded/,
|
|
|
|
|
);
|
|
|
|
|
});
|