284 lines
12 KiB
JavaScript
284 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawnSync } = require("child_process");
|
|
|
|
const packageRoot = path.resolve(__dirname, "..");
|
|
const requiredDigitalTables = [
|
|
"digital_plans",
|
|
"digital_plan_steps",
|
|
"digital_schedules",
|
|
"digital_schedule_runs",
|
|
"digital_tasks",
|
|
"digital_runs",
|
|
"digital_events",
|
|
"digital_artifacts",
|
|
"digital_approvals",
|
|
"digital_memories",
|
|
"digital_sync_outbox",
|
|
"digital_sync_attempts",
|
|
];
|
|
|
|
const testFiles = [
|
|
"src/main/services/digitalEmployee/stateService.test.ts",
|
|
"src/main/services/digitalEmployee/artifactAccessService.test.ts",
|
|
"src/main/services/digitalEmployee/schedulerService.test.ts",
|
|
"src/main/ipc/digitalEmployeeHandlers.test.ts",
|
|
"src/main/ipc/eventForwarders.test.ts",
|
|
"src/main/services/engines/acp/acpEngine.test.ts",
|
|
"src/main/services/digitalEmployee/syncService.test.ts",
|
|
"src/main/services/digitalEmployee/skillCatalogService.test.ts",
|
|
"src/main/services/digitalEmployee/planTemplateService.test.ts",
|
|
];
|
|
|
|
const selfHealingDigitalSchemaSql = `
|
|
CREATE TABLE IF NOT EXISTS digital_plan_steps (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
seq INTEGER NOT NULL,
|
|
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
preferred_skill_ids TEXT NOT NULL DEFAULT '[]',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_plan ON digital_plan_steps(plan_id, seq);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_sync ON digital_plan_steps(sync_status);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_schedules (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
plan_id TEXT,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
cron_expr TEXT NOT NULL,
|
|
timezone TEXT NOT NULL DEFAULT 'system',
|
|
status TEXT NOT NULL DEFAULT 'enabled',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
prompt TEXT,
|
|
command TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
next_run_at TEXT,
|
|
last_run_at TEXT,
|
|
catch_up_on_startup INTEGER NOT NULL DEFAULT 1,
|
|
max_catch_up_runs INTEGER NOT NULL DEFAULT 5,
|
|
max_run_history INTEGER NOT NULL DEFAULT 100,
|
|
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
deleted_at TEXT,
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS digital_schedule_runs (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
schedule_id TEXT NOT NULL,
|
|
plan_id TEXT,
|
|
run_id TEXT,
|
|
due_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
status TEXT NOT NULL,
|
|
attempt_no INTEGER NOT NULL DEFAULT 1,
|
|
trigger_kind TEXT NOT NULL DEFAULT 'scheduled',
|
|
error TEXT,
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(schedule_id) REFERENCES digital_schedules(id),
|
|
FOREIGN KEY(plan_id) REFERENCES digital_plans(id),
|
|
FOREIGN KEY(run_id) REFERENCES digital_runs(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_due ON digital_schedules(enabled, status, next_run_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_plan ON digital_schedules(plan_id);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedules_sync ON digital_schedules(sync_status);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_schedule ON digital_schedule_runs(schedule_id, due_at);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_digital_schedule_runs_attempt ON digital_schedule_runs(schedule_id, due_at, attempt_no);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_sync ON digital_schedule_runs(sync_status);
|
|
|
|
CREATE TABLE IF NOT EXISTS digital_memories (
|
|
id TEXT PRIMARY KEY,
|
|
remote_id TEXT,
|
|
key TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
category TEXT NOT NULL DEFAULT 'fact',
|
|
source TEXT NOT NULL DEFAULT 'qimingclaw',
|
|
source_path TEXT,
|
|
session_id TEXT,
|
|
score REAL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
last_synced_at TEXT,
|
|
sync_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
deleted_at TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_key ON digital_memories(key, status, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_category ON digital_memories(category, status, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_digital_memories_sync ON digital_memories(sync_status);
|
|
`;
|
|
|
|
function runStep(title, command, args, options = {}) {
|
|
console.log(`\n[DigitalEmployeeCheck] ${title}`);
|
|
const result = spawnSync(command, args, {
|
|
cwd: packageRoot,
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
...options,
|
|
});
|
|
if (result.status !== 0) {
|
|
process.exitCode = result.status || 1;
|
|
throw new Error(`${title} failed`);
|
|
}
|
|
}
|
|
|
|
function checkSgrobotDigitalAssets() {
|
|
console.log("\n[DigitalEmployeeCheck] Verify embedded digital assets");
|
|
const indexPath = path.join(packageRoot, "public", "sgrobot-digital", "index.html");
|
|
const html = fs.readFileSync(indexPath, "utf8");
|
|
const assetRefs = [...html.matchAll(/(?:src|href)="\.\/(assets\/[^"]+)"/g)]
|
|
.map((match) => match[1]);
|
|
if (assetRefs.length === 0) {
|
|
throw new Error("No embedded digital assets referenced by index.html");
|
|
}
|
|
for (const assetRef of assetRefs) {
|
|
const assetPath = path.join(packageRoot, "public", "sgrobot-digital", assetRef);
|
|
if (!fs.existsSync(assetPath)) {
|
|
throw new Error(`Missing embedded digital asset: ${assetRef}`);
|
|
}
|
|
}
|
|
console.log(`[DigitalEmployeeCheck] ${assetRefs.length} asset references OK`);
|
|
}
|
|
|
|
function checkDigitalDiagnosticAuditProjection() {
|
|
console.log("\n[DigitalEmployeeCheck] Verify diagnostic/audit projection hooks");
|
|
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
|
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
|
const pagePath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
|
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
|
const types = fs.readFileSync(typesPath, "utf8");
|
|
const page = fs.readFileSync(pagePath, "utf8");
|
|
const requiredSnippets = [
|
|
[adapter, "'audits'", "audit business view kind"],
|
|
[adapter, "buildDiagnosticSummary", "diagnostic summary builder"],
|
|
[adapter, "filterAuditRows", "audit filters"],
|
|
[adapter, "diagnosticNotifications", "diagnostic notifications"],
|
|
[adapter, "auditNotifications", "audit notifications"],
|
|
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
|
|
[types, "diagnostic_summary?: DigitalEmployeeDiagnosticSummary", "workday diagnostic field"],
|
|
[page, "de-diagnostic-summary-strip", "dashboard diagnostic strip"],
|
|
];
|
|
const missing = requiredSnippets
|
|
.filter(([content, snippet]) => !content.includes(snippet))
|
|
.map(([, , label]) => label);
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing diagnostic/audit projection hooks: ${missing.join(", ")}`);
|
|
}
|
|
console.log("[DigitalEmployeeCheck] diagnostic/audit projection hooks OK");
|
|
}
|
|
|
|
function checkDigitalNotificationPreferences() {
|
|
console.log("\n[DigitalEmployeeCheck] Verify notification preference hooks");
|
|
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
|
const apiPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "api.ts");
|
|
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
|
const shellPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "DigitalEmployeeShell.tsx");
|
|
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
|
const api = fs.readFileSync(apiPath, "utf8");
|
|
const types = fs.readFileSync(typesPath, "utf8");
|
|
const shell = fs.readFileSync(shellPath, "utf8");
|
|
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
|
const requiredSnippets = [
|
|
[adapter, "/api/notifications/preferences", "adapter notification preference endpoint"],
|
|
[adapter, "handleNotificationPreferencesPatch", "adapter notification preference patch handler"],
|
|
[adapter, "notificationMatchesPreferences", "adapter notification preference filter"],
|
|
[adapter, "update_notification_preferences", "adapter notification preference save action"],
|
|
[api, "getNotificationPreferences", "notification preferences API getter"],
|
|
[api, "patchNotificationPreferences", "notification preferences API patcher"],
|
|
[types, "NotificationPreferences", "notification preferences type"],
|
|
[shell, "de-notification-preferences", "notification preferences UI"],
|
|
[shell, "NOTIFICATION_KIND_OPTIONS", "notification kind mute UI options"],
|
|
[stateService, "update_notification_preferences", "notification preference runtime event"],
|
|
];
|
|
const missing = requiredSnippets
|
|
.filter(([content, snippet]) => !content.includes(snippet))
|
|
.map(([, , label]) => label);
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing notification preference hooks: ${missing.join(", ")}`);
|
|
}
|
|
console.log("[DigitalEmployeeCheck] notification preference hooks OK");
|
|
}
|
|
|
|
function checkLocalDatabaseSchema() {
|
|
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
|
|
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
|
|
if (!fs.existsSync(dbPath)) {
|
|
console.log(`[DigitalEmployeeCheck] Skip DB schema check; database not found: ${dbPath}`);
|
|
return;
|
|
}
|
|
|
|
const healResult = spawnSync("sqlite3", [dbPath, selfHealingDigitalSchemaSql], {
|
|
encoding: "utf8",
|
|
shell: process.platform === "win32",
|
|
});
|
|
if (healResult.error) {
|
|
console.log(`[DigitalEmployeeCheck] Skip DB schema check; sqlite3 unavailable: ${healResult.error.message}`);
|
|
return;
|
|
}
|
|
if (healResult.status !== 0) {
|
|
throw new Error(healResult.stderr || "sqlite3 digital schema self-heal failed");
|
|
}
|
|
|
|
const result = spawnSync("sqlite3", [dbPath, ".tables"], {
|
|
encoding: "utf8",
|
|
shell: process.platform === "win32",
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr || "sqlite3 .tables failed");
|
|
}
|
|
|
|
const tables = new Set(result.stdout.split(/\s+/).filter(Boolean));
|
|
const missing = requiredDigitalTables.filter((table) => !tables.has(table));
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing digital employee tables: ${missing.join(", ")}`);
|
|
}
|
|
console.log(`[DigitalEmployeeCheck] ${requiredDigitalTables.length} digital tables OK`);
|
|
}
|
|
|
|
function main() {
|
|
try {
|
|
runStep("Run digital employee unit tests", "npm", ["run", "test:run", "--", ...testFiles]);
|
|
runStep("Build Electron main process", "npm", ["run", "build:main:dev"]);
|
|
runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]);
|
|
checkSgrobotDigitalAssets();
|
|
checkDigitalDiagnosticAuditProjection();
|
|
checkDigitalNotificationPreferences();
|
|
checkLocalDatabaseSchema();
|
|
console.log("\n[DigitalEmployeeCheck] All checks passed");
|
|
} catch (error) {
|
|
console.error(`\n[DigitalEmployeeCheck] ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(process.exitCode || 1);
|
|
}
|
|
}
|
|
|
|
main();
|