feat(control-plane): add libSQL Turso backend

This commit is contained in:
Agent Board
2026-07-03 13:24:20 +08:00
parent ee6612028d
commit a75b3d11f9
44 changed files with 11070 additions and 666 deletions
+30 -54
View File
@@ -5,15 +5,18 @@ const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
ensureAuthenticated,
} = require("./tree-shell-smoke-helpers");
const {
setupWorkspaceAccess,
listAiRuntimeRuns,
listExternalConversationBindings,
} = require("./lib/control-plane-dev-seed");
const CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
const CHROMIUM_EXECUTABLE_PATH =
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
[
@@ -79,41 +82,6 @@ function writeWorkspaceManifest(root, ownerId, workspaceId) {
);
}
function sqlQuote(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
function sqliteExec(sql) {
execFileSync("sqlite3", [CONTROL_PLANE_DB, sql], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
}
function sqliteJson(sql, fallback = null) {
try {
const raw = execFileSync("sqlite3", ["-json", CONTROL_PLANE_DB, sql], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
}
function grantWorkspaceAccess({ actorId, workspaceId, root, rootUri, grantId }) {
const now = new Date().toISOString();
sqliteExec(`
INSERT OR IGNORE INTO users (id, email, username, display_name, role, status, created_at, updated_at, revision)
VALUES (${sqlQuote(actorId)}, ${sqlQuote(`${actorId}@example.com`)}, ${sqlQuote(actorId)}, ${sqlQuote(actorId)}, 'user', 'active', ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
INSERT OR REPLACE INTO workspaces (id, owner_user_id, name, kind, root_uri, root_path, source_kind, status, created_at, updated_at, revision)
VALUES (${sqlQuote(workspaceId)}, ${sqlQuote(actorId)}, ${sqlQuote(actorId)}, 'personal', ${sqlQuote(rootUri)}, ${sqlQuote(root)}, 'local_folder', 'active', ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
INSERT OR REPLACE INTO directory_grants (id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision)
VALUES (${sqlQuote(grantId)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(rootUri)}, ${sqlQuote(root)}, 'write', 1, '["ai","markdown_edit"]', 'smoke', 'active', ${sqlQuote(actorId)}, ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
`);
}
async function saveScreenshot(page, name) {
const target = path.join(OUTPUT_DIR, `${name}.png`);
await page.screenshot({ path: target, fullPage: false });
@@ -152,7 +120,7 @@ async function selectProvider(page, provider) {
);
}
async function runProviderSmoke(page, provider, suffix) {
async function runProviderSmoke(page, provider, suffix, { actorId, workspaceId }) {
const marker = `${provider.markerPrefix}_${suffix}`;
const runRequests = [];
const runResponses = [];
@@ -218,10 +186,13 @@ async function runProviderSmoke(page, provider, suffix) {
assert.strictEqual(markerAssistantCount, 1, `${provider.key} 可见 API 回复应只有一条`);
const sessionId = String(run.sessionId);
const bindingRows = sqliteJson(
`SELECT mnote_session_id, provider, status FROM ai_external_conversation_bindings WHERE user_id=${sqlQuote("mnote-e2e")} AND mnote_session_id=${sqlQuote(sessionId)} ORDER BY updated_at DESC LIMIT 5;`,
[],
);
const bindingRows = await listExternalConversationBindings(page.context().request, BASE_URL, {
userId: actorId,
workspaceId,
mnoteSessionId: sessionId,
limit: 5,
timeoutMs: UI_TIMEOUT_MS,
});
assert.strictEqual(bindingRows.length, 0, `${provider.key} API ChatOnly 不应写网页 provider conversation binding`);
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
@@ -259,10 +230,13 @@ async function runProviderSmoke(page, provider, suffix) {
`${provider.key} remoteDelete reason 应说明 API Chat 无远端会话`,
);
const remainingRows = sqliteJson(
`SELECT session_id, status FROM ai_runtime_runs WHERE user_id=${sqlQuote("mnote-e2e")} AND session_id=${sqlQuote(sessionId)} AND deleted_at IS NULL LIMIT 5;`,
[],
);
const remainingRows = await listAiRuntimeRuns(page.context().request, BASE_URL, {
userId: actorId,
workspaceId,
sessionId,
limit: 5,
timeoutMs: UI_TIMEOUT_MS,
});
assert.strictEqual(remainingRows.length, 0, `${provider.key} 删除后 SQLite active run 不应残留`);
return {
@@ -294,13 +268,6 @@ async function main() {
writeWorkspaceManifest(root, actorId, workspaceId);
fs.writeFileSync(path.join(root, relativePath), ["# API ChatOnly", "", `MNOTE_API_CHAT_WORKSPACE_${suffix}`, ""].join("\n"), "utf8");
grantWorkspaceAccess({
actorId,
workspaceId,
root,
rootUri,
grantId: `grant_task527_api_chat_${suffix}`,
});
const browser = await chromium.launch({
headless: true,
@@ -320,6 +287,15 @@ async function main() {
try {
await ensureAuthenticated(page, context.request);
await setupWorkspaceAccess(context.request, BASE_URL, {
actorId,
workspaceId,
workspaceName: actorId,
rootPath: root,
rootUri,
capabilities: ["ai", "markdown_edit"],
timeoutMs: UI_TIMEOUT_MS,
});
const response = await page.goto(documentUrl(root, relativePath), {
waitUntil: "domcontentloaded",
timeout: UI_TIMEOUT_MS,
@@ -333,7 +309,7 @@ async function main() {
await saveScreenshot(page, "initial-drawer");
for (const provider of PROVIDERS) {
results.push(await runProviderSmoke(page, provider, suffix));
results.push(await runProviderSmoke(page, provider, suffix, { actorId, workspaceId }));
}
} catch (error) {
caughtError = error;