feat(control-plane): add libSQL Turso backend
This commit is contained in:
@@ -7,23 +7,21 @@ const http = require("node:http");
|
||||
const net = require("node:net");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { spawn, execFileSync } = require("node:child_process");
|
||||
const { spawn } = require("node:child_process");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const CONTROL_PLANE_DB = process.env.MNOTE_CONTROL_PLANE_DB_PATH || "/mnt/Data1T/Mnote_data/control-plane/control-plane.db";
|
||||
const {
|
||||
setupWorkspaceAccess,
|
||||
seedAiRuntime,
|
||||
getAiRuntimeRun,
|
||||
} = require(path.join(ROOT, "scripts", "lib", "control-plane-dev-seed"));
|
||||
const OUT_DIR = process.env.MNOTE_PAGE_AI_SESSION_DASHBOARD_OUTPUT_DIR || fs.mkdtempSync(path.join(ROOT, "tmp", "task561-session-dashboard-"));
|
||||
const TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 90_000);
|
||||
const CHROME = process.env.MNOTE_PAGE_AI_CHROME || "/usr/bin/google-chrome";
|
||||
const ACTOR = "mnote-e2e";
|
||||
const PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!";
|
||||
|
||||
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) { const out = execFileSync("sqlite3", ["-json", CONTROL_PLANE_DB, sql], { encoding: "utf8" }); return out.trim() ? JSON.parse(out) : []; }
|
||||
function fileUrl(localPath) { return `file://${localPath}`; }
|
||||
function localMdDocumentId(relativePath) { return `local-md:${relativePath.replaceAll("/", "~2F")}`; }
|
||||
|
||||
function pickPort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
@@ -69,24 +67,6 @@ async function signIn(context, baseUrl) {
|
||||
return JSON.parse(await whoami.text());
|
||||
}
|
||||
|
||||
function seedWorkspaceAndSession({ actorId, workspaceId, root, rootUri, documentId, sessionId, runId, suffix }) {
|
||||
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)}, 'Task561 Session Dashboard', '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(`grant_task561_${suffix}`)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(rootUri)}, ${sqlQuote(root)}, 'write', 1, '["ai"]', 'smoke', 'active', ${sqlQuote(actorId)}, ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
|
||||
INSERT OR REPLACE INTO ai_runtime_runs (id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision)
|
||||
VALUES (${sqlQuote(`airun_${runId}`)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(documentId)}, ${sqlQuote(sessionId)}, ${sqlQuote(runId)}, ${sqlQuote(`Task561 Seed ${suffix}`)}, 'reasonix', 'reasonix', ${sqlQuote(`trace_${suffix}`)}, 'completed', ${sqlQuote(JSON.stringify({ status: 'completed', acpSessionId: `acp_${suffix}` }))}, ${sqlQuote(JSON.stringify({ message: `Task561 dashboard seed ${suffix}`, requestId: `req_${suffix}`, agentId: 'reasonix', acpRuntime: 'reasonix' }))}, NULL, ${sqlQuote(now)}, ${sqlQuote(now)}, 1);
|
||||
INSERT INTO ai_runtime_events (id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at)
|
||||
VALUES (${sqlQuote(`aievt_${runId}_1`)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(documentId)}, ${sqlQuote(sessionId)}, ${sqlQuote(runId)}, 'reasonix', 'reasonix', 'message.delta', ${sqlQuote(JSON.stringify({ delta: `Task561 assistant answer ${suffix}` }))}, ${sqlQuote(now)});
|
||||
INSERT INTO ai_runtime_events (id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at)
|
||||
VALUES (${sqlQuote(`aievt_${runId}_2`)}, ${sqlQuote(actorId)}, ${sqlQuote(workspaceId)}, ${sqlQuote(documentId)}, ${sqlQuote(sessionId)}, ${sqlQuote(runId)}, 'reasonix', 'reasonix', 'run.completed', ${sqlQuote(JSON.stringify({ stopReason: 'EndTurn' }))}, ${sqlQuote(now)});
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
const port = await pickPort();
|
||||
@@ -110,13 +90,50 @@ async function main() {
|
||||
const workspaceId = `local-ws:${actorId}:task561-${suffix}`;
|
||||
const sessionId = `mnote_task561_${suffix}`;
|
||||
const runId = `run_task561_${suffix}`;
|
||||
const rootUri = fileUrl(root);
|
||||
const rootUri = `file://${root}`;
|
||||
const relativePath = `Task561-${suffix}.md`;
|
||||
const documentId = localMdDocumentId(relativePath);
|
||||
const documentId = `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||
fs.mkdirSync(path.join(root, ".mnote"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, ".mnote", "workspace.json"), `${JSON.stringify({ workspaceId, ownerId: actorId }, null, 2)}\n`, "utf8");
|
||||
fs.writeFileSync(path.join(root, relativePath), `# Task561 Dashboard\n\n${suffix}\n`, "utf8");
|
||||
seedWorkspaceAndSession({ actorId, workspaceId, root, rootUri, documentId, sessionId, runId, suffix });
|
||||
// 通过 API helper 写入 workspace/用户/授权 + run/events,不再直写 SQLite
|
||||
await setupWorkspaceAccess(context.request, baseUrl, {
|
||||
actorId,
|
||||
email: `${actorId}@example.com`,
|
||||
username: actorId,
|
||||
displayName: actorId,
|
||||
role: "user",
|
||||
workspaceId,
|
||||
workspaceName: "Task561 Session Dashboard",
|
||||
rootUri,
|
||||
rootPath: root,
|
||||
sourceKind: "local_folder",
|
||||
permission: "write",
|
||||
capabilities: ["ai"],
|
||||
grantSource: "smoke",
|
||||
grantCreatedBy: actorId,
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
});
|
||||
await seedAiRuntime(context.request, baseUrl, {
|
||||
id: `airun_${runId}`,
|
||||
userId: actorId,
|
||||
workspaceId,
|
||||
documentId,
|
||||
sessionId,
|
||||
runId,
|
||||
title: `Task561 Seed ${suffix}`,
|
||||
profile: "reasonix",
|
||||
acpRuntime: "reasonix",
|
||||
traceId: `trace_${suffix}`,
|
||||
status: "completed",
|
||||
runtimeJson: { status: "completed", acpSessionId: `acp_${suffix}` },
|
||||
payloadJson: { message: `Task561 dashboard seed ${suffix}`, requestId: `req_${suffix}`, agentId: "reasonix", acpRuntime: "reasonix" },
|
||||
events: [
|
||||
{ id: `aievt_${runId}_1`, eventType: "message.delta", payloadJson: { delta: `Task561 assistant answer ${suffix}` } },
|
||||
{ id: `aievt_${runId}_2`, eventType: "run.completed", payloadJson: { stopReason: "EndTurn" } },
|
||||
],
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const url = new URL(`${baseUrl}/documents/${encodeURIComponent(documentId)}`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
@@ -177,8 +194,10 @@ async function main() {
|
||||
await page.evaluate(() => { window.confirm = () => true; });
|
||||
await page.locator(`[data-page-ai-session-delete="${sessionId}"]`).click({ timeout: TIMEOUT_MS });
|
||||
await page.waitForFunction((id) => !document.querySelector(`[data-page-ai-session-row="${id}"]`), sessionId, { timeout: TIMEOUT_MS });
|
||||
const deletedRows = sqliteJson(`SELECT deleted_at AS deletedAt FROM ai_runtime_runs WHERE run_id=${sqlQuote(runId)};`);
|
||||
assert(deletedRows[0] && deletedRows[0].deletedAt, "delete 应软删除 SQLite session runs");
|
||||
// 通过 API helper 验证软删除,不再直读 SQLite
|
||||
const deletedRun = await getAiRuntimeRun(context.request, baseUrl, { userId: actorId, runId });
|
||||
assert(deletedRun, `run ${runId} 应仍 persist(软删除)`);
|
||||
assert(deletedRun.deletedAt, "delete 应软删除 SQLite session runs");
|
||||
const screenshotPath = path.join(OUT_DIR, "task561-session-dashboard.png");
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const result = { ok: true, baseUrl, outDir: OUT_DIR, workspaceId, documentId, sessionId, runId, settingsScreenshotPath, beforeDeleteScreenshotPath, screenshotPath };
|
||||
|
||||
Reference in New Issue
Block a user