Files
storyforge/web/storyforge-web-v4/tests/workbench-pages.test.mjs
2026-03-29 19:31:28 +08:00

293 lines
18 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const ROOT = path.resolve(process.cwd(), "web/storyforge-web-v4");
const HTML = fs.readFileSync(path.join(ROOT, "index.html"), "utf8");
const APP = fs.readFileSync(path.join(ROOT, "assets/app.js"), "utf8");
const CSS = fs.readFileSync(path.join(ROOT, "assets/styles.css"), "utf8");
function extractBetween(source, startToken, endToken) {
const start = source.indexOf(startToken);
assert.notEqual(start, -1, `Missing token: ${startToken}`);
const end = source.indexOf(endToken, start);
assert.notEqual(end, -1, `Missing token: ${endToken}`);
return source.slice(start, end);
}
test("settings navigation and screen are real routes", () => {
assert.match(HTML, /data-screen-target="settings"/);
assert.match(HTML, /data-screen="settings"/);
assert.match(APP, /function renderSettingsScreen\(/);
assert.match(APP, /screenMap\.settings\.innerHTML = renderSettingsScreen\(\);/);
assert.match(APP, /window\.addEventListener\("hashchange"/);
});
test("strategy navigation and screen are real routes", () => {
assert.match(HTML, /data-screen-target="strategy"/);
assert.match(HTML, /data-screen="strategy"/);
assert.match(APP, /function renderStrategyScreen\(/);
assert.match(APP, /screenMap\.strategy\.innerHTML = renderStrategyScreen\(\);/);
});
test("automation screen stays user-facing and excludes admin-only panels", () => {
const source = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
assert.doesNotMatch(source, /renderTenantQuotaPanel\(/);
assert.doesNotMatch(source, /renderOneLinerActionRegistryPanel\(/);
assert.doesNotMatch(source, /renderAdminOpsPanel\(/);
assert.match(source, /renderDetailTabs\("automationDetailTab"/);
});
test("agent screen excludes quota and registry panels and uses page tabs", () => {
const source = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
assert.doesNotMatch(source, /renderTenantQuotaPanel\(/);
assert.doesNotMatch(source, /renderOneLinerActionRegistryPanel\(/);
assert.match(source, /renderDetailTabs\("playbookDetailTab"/);
assert.match(source, /renderGovernanceSummaryCard\(/);
assert.match(source, /open-user-global-policy/);
assert.match(source, /open-user-platform-policy/);
assert.match(source, /active_admin_override_notice/);
});
test("discovery, production, and admin screens use page tabs for heavy content", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
const admin = extractBetween(APP, "function renderAdminWorkbenchScreen()", "function renderDashboardScreen()");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
assert.match(admin, /覆盖与审计/);
assert.match(strategy, /renderDetailTabs\("strategyDetailTab"/);
assert.match(strategy, /renderPolicyAuditFeed\(/);
});
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {
const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
assert.match(projects, /project-status-grid/);
assert.match(CSS, /\.project-status-grid\s*\{[\s\S]*repeat\(auto-fit,\s*minmax\(260px,\s*1fr\)\)/);
assert.match(CSS, /\.entity-card\.pad\s*\{[\s\S]*padding:\s*15px/);
});
test("dashboard and project screens distinguish auto-connecting from truly disconnected", () => {
const dashboard = extractBetween(APP, "function renderDashboardScreen()", "function renderProjectsScreen()");
const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
const helper = extractBetween(APP, "function isAutoConnectionPending()", "async function refreshFromAuthModal()");
assert.match(APP, /function isAutoConnectionPending\(\)/);
assert.match(APP, /function renderAutoConnectingScreen\(/);
assert.match(helper, /正在自动连接工作区/);
assert.match(dashboard, /isAutoConnectionPending\(\)/);
assert.match(dashboard, /renderAutoConnectingScreen\("项目总台"/);
assert.match(projects, /isAutoConnectionPending\(\)/);
assert.match(projects, /renderAutoConnectingScreen\("我的项目"/);
});
test("ensureAutoSession re-renders immediately when auto-connect starts", () => {
const source = extractBetween(APP, "async function ensureAutoSession(options = {})", "async function refreshFromAuthModal()");
assert.match(source, /appState\.autoConnectAttempted = true;/);
assert.match(source, /renderAll\(\);/);
});
test("bootstrap does not trust a stored session from a different backend", () => {
const helpers = extractBetween(APP, "function loadStoredSession()", "function compareDateDesc(");
const ensure = extractBetween(APP, "async function ensureAutoSession(options = {})", "async function refreshFromAuthModal()");
const bootstrap = extractBetween(APP, "async function bootstrap()", "async function markTrackingDigestRead()");
assert.match(helpers, /function normalizeBackendUrlValue\(/);
assert.match(helpers, /function hasSessionBackendMismatch\(/);
assert.match(ensure, /const backendMismatch = hasSessionBackendMismatch\(backendUrl\);/);
assert.match(ensure, /persistSession\(null\);/);
assert.match(bootstrap, /const backendMismatch = hasSessionBackendMismatch\(\);/);
assert.match(bootstrap, /await ensureAutoSession\(\{ force: backendMismatch \}\);/);
});
test("oneliner submit failures stay inside the app instead of using a browser alert", () => {
assert.doesNotMatch(APP, /alert\("OneLiner 调度失败:/);
assert.match(APP, /presentActionFailure\(error,\s*"OneLiner 调度失败"\)/);
});
test("agent control surfaces load governance endpoints for user and admin summaries", () => {
const source = extractBetween(APP, "async function loadAgentControlSurfaces(projectId = \"\")", "async function loadOneLinerMessages(sessionId)");
assert.match(source, /\/v2\/oneliner\/governance\/effective/);
assert.match(source, /\/v2\/oneliner\/runs/);
assert.match(APP, /function hydrateSelectedOneLinerRun\(\)/);
assert.match(APP, /\/v2\/oneliner\/runs\/\$\{encodeURIComponent\(runId\)\}/);
assert.match(source, /\/v2\/oneliner\/governance\/user\/global/);
assert.match(source, /\/v2\/oneliner\/governance\/user\/platforms\/\$\{encodeURIComponent\(governancePlatform\)\}/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/system\/main-agent/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/system\/platforms\/\$\{encodeURIComponent\(item\.value\)\}/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/directory/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/overrides/);
assert.match(source, /Object\.prototype\.hasOwnProperty\.call\(existingTarget, "targetProjectId"\)/);
assert.match(source, /const requestedProjectId = hasExistingProjectTarget/);
assert.match(source, /const targetProjectId = requestedProjectId === ""/);
});
test("oneliner panel includes a dedicated runtime header for agent runs", () => {
const source = extractBetween(APP, "function ensureOneLinerUi()", "function renderOneLinerSessionTabs()");
const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()");
assert.match(source, /data-role="oneliner-runs"/);
assert.match(runtime, /confirm-oneliner-run/);
assert.match(runtime, /cancel-oneliner-run/);
assert.match(runtime, /当前计划/);
assert.match(runtime, /recommended_preview_action/);
assert.match(runtime, /renderOneLinerExecutionPayloadHtml\(currentRun\.result\)/);
assert.match(runtime, /open-oneliner-run-result/);
assert.match(runtime, /recommended_action/);
});
test("oneliner meta and action handlers expose governance entry points", () => {
const meta = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()");
const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderOneLinerUi()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(meta, /open-user-global-policy/);
assert.match(meta, /renderOneLinerRunsHtml\(\)/);
assert.match(meta, /policyScopeTagLabel/);
assert.match(messages, /active_admin_override_notice/);
assert.match(actions, /name === "open-user-global-policy"/);
assert.match(actions, /name === "open-system-main-policy"/);
assert.match(actions, /name === "handoff-to-main-agent"/);
assert.match(actions, /name === "confirm-oneliner-run"/);
assert.match(actions, /name === "open-oneliner-run-result"/);
});
test("oneliner runtime remembers completed runs exactly once after hydration", () => {
const hydrate = extractBetween(APP, "async function hydrateSelectedOneLinerRun()", "async function loadAgentControlSurfaces(projectId = \"\")");
const state = extractBetween(APP, "const appState = {", "};\n\nlet PLATFORM_RUNTIME");
assert.match(state, /lastCompletedOnelinerRunId/);
assert.match(hydrate, /detail\.run_status === "done"/);
assert.match(hydrate, /appState\.lastCompletedOnelinerRunId !== detail\.id/);
assert.match(hydrate, /rememberAction\("主 Agent 已完成本轮"/);
});
test("system governance saves refresh control surfaces after persisting", () => {
const profile = extractBetween(APP, "function openOneLinerProfileAction()", "function parsePolicyJsonField(rawValue, label = \"策略 JSON\")");
const userGlobal = extractBetween(APP, "function openUserGlobalPolicyAction()", "function openUserPlatformPolicyAction(platform)");
const userPlatform = extractBetween(APP, "function openUserPlatformPolicyAction(platform)", "function openSystemMainPolicyAction()");
const main = extractBetween(APP, "function openSystemMainPolicyAction()", "function openSystemPlatformPolicyAction(platform)");
const platform = extractBetween(APP, "function openSystemPlatformPolicyAction(platform)", "function openPlatformAgentProfileAction(platform)");
assert.match(profile, /appState\.onelinerProfile = saved;/);
assert.match(profile, /await loadAgentControlSurfaces\(project\.id\);/);
assert.match(userGlobal, /appState\.userGlobalPolicy = saved;/);
assert.match(userGlobal, /await loadAgentControlSurfaces\(project\.id\);/);
assert.match(userPlatform, /appState\.userCurrentPlatformPolicy = saved;/);
assert.match(userPlatform, /await loadAgentControlSurfaces\(project\.id\);/);
assert.match(main, /const projectId = getOneLinerProjectId\(\);/);
assert.match(main, /appState\.adminSystemMainPolicy = saved;/);
assert.match(main, /await loadAgentControlSurfaces\(projectId\);/);
assert.match(platform, /const projectId = getOneLinerProjectId\(\);/);
assert.match(platform, /appState\.adminSystemPlatformPolicies = safeArray\(appState\.adminSystemPlatformPolicies\)/);
assert.match(platform, /await loadAgentControlSurfaces\(projectId\);/);
});
test("governance UI exposes admin override target picker and history rollback entrypoints", () => {
const admin = extractBetween(APP, "function renderAdminGovernanceSummaryPanel()", "function renderPlatformAgentPanel()");
const targetPicker = extractBetween(APP, "async function openAdminOverrideTargetAction()", "function openAdminOverridePolicyAction()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(admin, /open-admin-override-target/);
assert.match(admin, /open-admin-override-policy/);
assert.match(admin, /open-admin-override-history/);
assert.match(admin, /open-system-main-policy-history/);
assert.match(admin, /open-system-platform-policy-history/);
assert.match(APP, /function syncAdminOverrideProjectOptions\(/);
assert.match(targetPicker, /onOpen: \(\) => \{/);
assert.match(targetPicker, /userSelect\.addEventListener\("change"/);
assert.match(targetPicker, /syncAdminOverrideProjectOptions\(userSelect\.value, ""\)/);
assert.match(actions, /name === "open-admin-override-target"/);
assert.match(actions, /name === "open-admin-override-policy"/);
assert.match(actions, /name === "open-admin-override-history"/);
assert.match(actions, /name === "open-system-main-policy-history"/);
assert.match(actions, /name === "open-system-platform-policy-history"/);
});
test("user governance UI exposes personal history and rollback entrypoints", () => {
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(playbook, /open-user-global-policy-history/);
assert.match(playbook, /open-user-platform-policy-history/);
assert.match(playbook, /handoff-to-main-agent/);
assert.match(playbook, /playbook-main-agent-handoff/);
assert.match(strategy, /active_admin_override_notice/);
assert.match(strategy, /管理员覆盖生效中/);
assert.match(strategy, /handoff-to-main-agent/);
assert.match(strategy, /strategy-main-agent-handoff/);
assert.match(automation, /handoff-to-main-agent/);
assert.match(automation, /automation-main-agent-handoff/);
assert.match(actions, /name === "open-user-global-policy-history"/);
assert.match(actions, /name === "open-user-platform-policy-history"/);
});
test("main agent result rendering offers a direct route back into the recommended screen", () => {
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
const lastAction = extractBetween(APP, "function renderLastActionCard()", "function getJobRecoveryCategory(job)");
assert.match(execution, /recommended_action/);
assert.match(execution, /data-action="\$\{escapeHtml\(payload\.recommended_action\.action\)\}"/);
assert.match(lastAction, /open-oneliner-run-result/);
assert.match(lastAction, /recommended_action/);
});
test("main agent route actions keep landing context and destination screens render a notice", () => {
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
assert.match(execution, /data-main-agent-run-id/);
assert.match(actions, /captureMainAgentLandingContext\(action,\s*"goto-production"/);
assert.match(actions, /captureMainAgentLandingContext\(action,\s*"goto-strategy"/);
assert.match(actions, /name === "dismiss-main-agent-landing"/);
assert.match(strategy, /renderMainAgentLandingNotice\("strategy"\)/);
assert.match(production, /renderMainAgentLandingNotice\("production"\)/);
});
test("key workbench screens expose contextual handoff-to-main-agent actions", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()");
const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
assert.match(discovery, /buildMainAgentHandoffAttrs\(\{/);
assert.match(discovery, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(discovery, /sourceScreen: "discovery"/);
assert.match(tracking, /buildMainAgentHandoffAttrs\(\{/);
assert.match(tracking, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(tracking, /sourceScreen: "tracking"/);
assert.match(projects, /buildMainAgentHandoffAttrs\(\{/);
assert.match(projects, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(projects, /sourceScreen: "intake"/);
assert.match(production, /buildMainAgentHandoffAttrs\(\{/);
assert.match(production, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(production, /sourceScreen: "production"/);
assert.match(review, /buildMainAgentHandoffAttrs\(\{/);
assert.match(review, /button\("交给主 Agent", "handoff-to-main-agent"/);
assert.match(review, /sourceScreen: "review"/);
});
test("oneliner runtime shows grouped run health summary above the current run card", () => {
const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()");
assert.match(runtime, /近期运行概况/);
assert.match(runtime, /待确认/);
assert.match(runtime, /执行中/);
assert.match(runtime, /已完成/);
assert.match(runtime, /safeArray\(runs\)\.filter\(\(item\) => item\.run_status === "needs_confirmation"\)\.length/);
});