feat(control-plane): add libSQL Turso backend
This commit is contained in:
@@ -5,13 +5,16 @@ 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,
|
||||
findExternalConversationBinding,
|
||||
} = require("./lib/control-plane-dev-seed");
|
||||
|
||||
const PROVIDERS = {
|
||||
deepseek: {
|
||||
@@ -44,7 +47,6 @@ if (!provider) {
|
||||
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", provider.taskName);
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const CONTROL_PLANE_DB = "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
||||
const CHROMIUM_EXECUTABLE_PATH =
|
||||
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
||||
[
|
||||
@@ -84,41 +86,6 @@ function writeWorkspaceManifest(root, ownerId, workspaceId) {
|
||||
);
|
||||
}
|
||||
|
||||
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 sqlQuote(value) {
|
||||
return `'${String(value).replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
function sqliteExec(sql) {
|
||||
execFileSync("sqlite3", [CONTROL_PLANE_DB, sql], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
`);
|
||||
}
|
||||
|
||||
function logSize(logPath) {
|
||||
try {
|
||||
return fs.statSync(logPath).size;
|
||||
@@ -298,13 +265,6 @@ async function main() {
|
||||
|
||||
writeWorkspaceManifest(root, actorId, workspaceId);
|
||||
fs.writeFileSync(path.join(root, relativePath), [`# ${provider.title}`, "", marker, ""].join("\n"), "utf8");
|
||||
grantWorkspaceAccess({
|
||||
actorId,
|
||||
workspaceId,
|
||||
root,
|
||||
rootUri,
|
||||
grantId: `grant_task513_${providerKey}_${suffix}`,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
@@ -338,6 +298,15 @@ async function main() {
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -393,13 +362,16 @@ async function main() {
|
||||
assert.strictEqual(markerAssistantCount, 1, "MNote 可见 provider 回复应只有一条");
|
||||
|
||||
const sessionId = String(run.sessionId);
|
||||
const bindingRows = sqliteJson(
|
||||
`SELECT mnote_session_id, remote_conversation_id, status FROM ai_external_conversation_bindings WHERE user_id=${sqlQuote(actorId)} AND mnote_session_id=${sqlQuote(sessionId)} AND provider=${sqlQuote(provider.expectedProvider)} ORDER BY updated_at DESC LIMIT 1;`,
|
||||
[],
|
||||
);
|
||||
assert(bindingRows && bindingRows.length === 1, "SQLite 应保存远端会话绑定");
|
||||
assert.strictEqual(bindingRows[0].status, "active", "删除前 binding 应为 active");
|
||||
const remoteConversationId = bindingRows[0].remote_conversation_id;
|
||||
const bindingBefore = await findExternalConversationBinding(context.request, BASE_URL, {
|
||||
userId: actorId,
|
||||
workspaceId,
|
||||
mnoteSessionId: sessionId,
|
||||
provider: provider.expectedProvider,
|
||||
timeoutMs: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert(bindingBefore, "control-plane 应保存远端会话绑定");
|
||||
assert.strictEqual(bindingBefore.status, "active", "删除前 binding 应为 active");
|
||||
const remoteConversationId = bindingBefore.remoteConversationId;
|
||||
assert(remoteConversationId, "binding 应包含 remote_conversation_id");
|
||||
|
||||
providerCaptures.afterMessage = await captureProvider(remoteConversationId, `${providerKey}-after-message`);
|
||||
@@ -449,12 +421,15 @@ async function main() {
|
||||
"provider 删除后网页历史中不应继续存在本轮远端会话链接",
|
||||
);
|
||||
|
||||
const deletedRows = sqliteJson(
|
||||
`SELECT mnote_session_id, remote_conversation_id, status, metadata_json FROM ai_external_conversation_bindings WHERE user_id=${sqlQuote(actorId)} AND mnote_session_id=${sqlQuote(sessionId)} AND provider=${sqlQuote(provider.expectedProvider)} ORDER BY updated_at DESC LIMIT 1;`,
|
||||
[],
|
||||
);
|
||||
assert(deletedRows && deletedRows.length === 1, "删除后 binding 仍应可审计");
|
||||
assert.strictEqual(deletedRows[0].status, "remote_deleted", "删除后 binding 应标记 remote_deleted");
|
||||
const bindingAfter = await findExternalConversationBinding(context.request, BASE_URL, {
|
||||
userId: actorId,
|
||||
workspaceId,
|
||||
mnoteSessionId: sessionId,
|
||||
provider: provider.expectedProvider,
|
||||
timeoutMs: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert(bindingAfter, "删除后 binding 仍应可审计");
|
||||
assert.strictEqual(bindingAfter.status, "remote_deleted", "删除后 binding 应标记 remote_deleted");
|
||||
|
||||
const providerLog = readLogSince(provider.gatewayLog, gatewayLogOffset);
|
||||
let providerSendCount = countMatches(providerLog, provider.sendLogPattern);
|
||||
@@ -474,8 +449,8 @@ async function main() {
|
||||
remoteConversationId,
|
||||
providerSendCount,
|
||||
deleteResponse: deleteResponses.at(-1),
|
||||
bindingBefore: bindingRows[0],
|
||||
bindingAfter: deletedRows[0],
|
||||
bindingBefore,
|
||||
bindingAfter,
|
||||
screenshots,
|
||||
providerCaptures,
|
||||
}, null, 2)}\n`,
|
||||
|
||||
Reference in New Issue
Block a user