feat(page-ai): add resumable run journal descriptors
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { request } = require("playwright");
|
||||
|
||||
const TASK = "task557-page-ai-agent-descriptor-smoke";
|
||||
const BASE_URL = (
|
||||
process.env.MNOTE_UI_BASE_URL ||
|
||||
process.env.MNOTE_WEB_SMOKE_BASE_URL ||
|
||||
"http://127.0.0.1:3000"
|
||||
).replace(/\/+$/, "");
|
||||
const ACTOR_ID = process.env.MNOTE_PAGE_AI_DESCRIPTOR_ACTOR_ID || `task557-descriptor-${Date.now().toString(36)}`;
|
||||
const PROFILE = process.env.MNOTE_PAGE_AI_DESCRIPTOR_PROFILE || "task557-descriptor-smoke";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
|
||||
|
||||
function descriptorById(payload) {
|
||||
return new Map(
|
||||
(payload.descriptors || []).map((descriptor) => [
|
||||
descriptor.agentId,
|
||||
descriptor,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function arrayIncludes(value, expected, message) {
|
||||
assert(Array.isArray(value), `${message}: 不是数组`);
|
||||
assert(value.includes(expected), `${message}: 缺少 ${expected}`);
|
||||
}
|
||||
|
||||
function toolsByName(descriptor) {
|
||||
return new Map(
|
||||
(descriptor.tools || []).map((tool) => [
|
||||
tool.name,
|
||||
tool,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDescriptors(api) {
|
||||
const response = await api.get(`/api/page-ai/agents/descriptors?profile=${encodeURIComponent(PROFILE)}`, {
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch (error) {
|
||||
throw new Error(`descriptor response 不是 JSON: ${text.slice(0, 1000)}`);
|
||||
}
|
||||
assert(response.ok(), `descriptor API 失败: ${response.status()} ${text.slice(0, 1000)}`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const api = await request.newContext({
|
||||
baseURL: BASE_URL,
|
||||
extraHTTPHeaders: {
|
||||
"x-mnote-actor-id": ACTOR_ID,
|
||||
"x-mnote-actor-type": "user",
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await loadDescriptors(api);
|
||||
assert.equal(payload.schema, "mnote.ai_agent_descriptors.v1", "顶层 schema 不正确");
|
||||
assert.equal(payload.profile, PROFILE, "profile 应回显 query profile");
|
||||
assert.equal(payload.actorId, ACTOR_ID, "actorId 应使用请求 actor");
|
||||
assert(Array.isArray(payload.descriptors), "descriptors 必须是数组");
|
||||
assert.equal(payload.descriptors.length, 3, "应返回 Hermes / Reasonix / Chat-only 三类 agent");
|
||||
|
||||
const descriptors = descriptorById(payload);
|
||||
for (const agentId of ["hermes", "reasonix", "chat_only"]) {
|
||||
assert(descriptors.has(agentId), `缺少 ${agentId} descriptor`);
|
||||
assert.equal(descriptors.get(agentId).schema, "mnote.ai_agent_descriptor.v1", `${agentId} descriptor schema 不正确`);
|
||||
}
|
||||
|
||||
const hermes = descriptors.get("hermes");
|
||||
assert.equal(hermes.provider, "hermes_client", "Hermes provider 不正确");
|
||||
assert.equal(hermes.canWriteFiles, true, "Hermes 应允许文件写入能力声明");
|
||||
arrayIncludes(hermes.capabilities, "mnote_tools", "Hermes capabilities");
|
||||
assert(toolsByName(hermes).has("mnote.knowledge_rag.query"), "Hermes tools 应包含 knowledge_rag.query");
|
||||
|
||||
const reasonix = descriptors.get("reasonix");
|
||||
assert.equal(reasonix.provider, "acp_reasonix", "Reasonix provider 不正确");
|
||||
assert.equal(reasonix.acpRuntime, "reasonix", "Reasonix acpRuntime 不正确");
|
||||
assert.equal(reasonix.canWriteFiles, true, "Reasonix 应允许文件写入能力声明");
|
||||
arrayIncludes(reasonix.capabilities, "native_patch", "Reasonix capabilities");
|
||||
arrayIncludes(reasonix.capabilities, "knowledge_rag", "Reasonix capabilities");
|
||||
const reasonixTools = toolsByName(reasonix);
|
||||
for (const toolName of [
|
||||
"mnote.knowledge_rag.status",
|
||||
"mnote.knowledge_rag.query",
|
||||
"mnote.knowledge_rag.open_reference",
|
||||
]) {
|
||||
assert(reasonixTools.has(toolName), `Reasonix tools 缺少 ${toolName}`);
|
||||
assert.equal(reasonixTools.get(toolName).enabled, true, `${toolName} 默认应启用`);
|
||||
}
|
||||
assert(
|
||||
(reasonix.capabilityPacks || []).some((capability) => capability.id === "mnote-knowledge-rag" && capability.enabled === true),
|
||||
"Reasonix capabilityPacks 应包含启用的 mnote-knowledge-rag",
|
||||
);
|
||||
assert.equal(reasonix.capabilityStates?.knowledge_rag?.enabled, true, "Reasonix capabilityStates 应声明 knowledge_rag 默认启用");
|
||||
|
||||
const chatOnly = descriptors.get("chat_only");
|
||||
assert.equal(chatOnly.provider, "chat_only", "Chat-only provider 不正确");
|
||||
assert.equal(chatOnly.canWriteFiles, false, "Chat-only 不应声明文件写入");
|
||||
assert.equal((chatOnly.tools || []).length, 0, "Chat-only 不应暴露 MNote tools");
|
||||
arrayIncludes(chatOnly.capabilities, "chat", "Chat-only capabilities");
|
||||
assert.deepEqual(chatOnly.defaultContextRefs || [], [], "Chat-only 默认不应携带 MNote context refs");
|
||||
|
||||
const toggleResponse = await api.put("/api/hermes/client/capabilities/toggle", {
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
headers: { "content-type": "application/json" },
|
||||
data: {
|
||||
runtime: "mnote",
|
||||
profile: PROFILE,
|
||||
id: "mnote-knowledge-rag",
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
const toggleText = await toggleResponse.text();
|
||||
assert(toggleResponse.ok(), `禁用 knowledge rag capability 失败: ${toggleResponse.status()} ${toggleText.slice(0, 1000)}`);
|
||||
const disabledPayload = await loadDescriptors(api);
|
||||
const disabledReasonix = descriptorById(disabledPayload).get("reasonix");
|
||||
assert.equal(disabledReasonix.capabilityStates?.knowledge_rag?.enabled, false, "禁用后 descriptor capabilityStates 应为 false");
|
||||
assert(!(disabledReasonix.capabilities || []).includes("knowledge_rag"), "禁用后 Reasonix capabilities 不应继续声明 knowledge_rag");
|
||||
assert((disabledReasonix.disabledCapabilities || []).includes("knowledge_rag"), "禁用后 disabledCapabilities 应包含 knowledge_rag");
|
||||
assert.equal(toolsByName(disabledReasonix).get("mnote.knowledge_rag.query")?.enabled, false, "禁用后 knowledge_rag.query tool 应不可用");
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
profile: PROFILE,
|
||||
actorId: ACTOR_ID,
|
||||
agents: payload.descriptors.map((descriptor) => ({
|
||||
agentId: descriptor.agentId,
|
||||
provider: descriptor.provider,
|
||||
canWriteFiles: descriptor.canWriteFiles,
|
||||
capabilityCount: (descriptor.capabilities || []).length,
|
||||
toolCount: (descriptor.tools || []).length,
|
||||
})),
|
||||
disabledKnowledgeRagVerified: true,
|
||||
};
|
||||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} finally {
|
||||
await api.dispose().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, "failure.json"),
|
||||
`${JSON.stringify({ ok: false, task: TASK, error: error.stack || error.message || String(error) }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user