chore: sync storyforge handoff state
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,12 @@
|
||||
function makePlatformRoutes(platform) {
|
||||
return {
|
||||
accounts: `/v2/${platform}/accounts`,
|
||||
syncAccount: `/v2/${platform}/accounts/sync`,
|
||||
workspace: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/workspace`,
|
||||
snapshots: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/snapshots`,
|
||||
snapshotDetail: (accountId, snapshotId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/snapshots/${encodeURIComponent(snapshotId)}`,
|
||||
creatorFields: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/creator-fields`,
|
||||
analysisReports: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis-reports`,
|
||||
videos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos?limit=80`,
|
||||
analyzeAccount: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis`,
|
||||
analyzeTopVideos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos/analyze-top`,
|
||||
|
||||
@@ -115,10 +115,18 @@ test("mobile action sheets and oneliner runtime behave like bottom sheets", () =
|
||||
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-composer\s*\{[\s\S]*position:\s*sticky/);
|
||||
});
|
||||
|
||||
test("opening OneLiner clears the transient loading state after the panel is hydrated", () => {
|
||||
test("opening OneLiner keeps global loading clear and uses panel-local hydration status", () => {
|
||||
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
assert.match(actions, /name === "open-oneliner"[\s\S]*setBusy\(true,\s*"正在打开 OneLiner\.\.\."\)/);
|
||||
assert.match(actions, /name === "open-oneliner"[\s\S]*finally \{[\s\S]*setBusy\(false,\s*""\);[\s\S]*renderAll\(\);[\s\S]*\}/);
|
||||
const branch = extractBetween(actions, "if (name === \"open-oneliner\") {", "if (name === \"close-oneliner\") {");
|
||||
assert.match(APP, /function setOneLinerHydrating\(next,\s*message = ""\)/);
|
||||
assert.match(branch, /setOneLinerHydrating\(true,\s*"正在同步 OneLiner 上下文\.\.\."\)/);
|
||||
assert.doesNotMatch(branch, /setBusy\(true,\s*"正在打开 OneLiner\.\.\."\)/);
|
||||
assert.match(branch, /finally \{[\s\S]*setOneLinerHydrating\(false,\s*""\);[\s\S]*renderAll\(\);[\s\S]*\}/);
|
||||
});
|
||||
|
||||
test("opening OneLiner opens the panel before waiting for control-surface hydration", () => {
|
||||
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
assert.match(actions, /name === "open-oneliner"[\s\S]*setOneLinerHydrating\(true,\s*"正在同步 OneLiner 上下文\.\.\."\)[\s\S]*openOneLinerPanel\(\);[\s\S]*renderAll\(\);[\s\S]*await loadAgentControlSurfaces/);
|
||||
});
|
||||
|
||||
test("project creation and switching use in-app sheets instead of browser prompts", () => {
|
||||
@@ -295,6 +303,57 @@ test("admin workbench exposes a dedicated model access workspace and actions", (
|
||||
assert.match(clickActions, /name === "open-admin-huobao-ai-config"/);
|
||||
});
|
||||
|
||||
test("owned account and admin model pages surface creator-center and model setup quick actions", () => {
|
||||
const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
|
||||
const modelOverview = extractBetween(APP, "function renderAdminModelCapabilityOverviewPanel()", "function renderAdminModelAccessPanel()");
|
||||
|
||||
assert.match(owned, /创作者中心账号分析/);
|
||||
assert.match(owned, /登录抖音创作者中心/);
|
||||
assert.match(owned, /登录快手创作者中心/);
|
||||
assert.match(owned, /open-creator-center-sync/);
|
||||
assert.match(owned, /data-platform="douyin" data-sync-origin="owned"/);
|
||||
assert.match(owned, /data-platform="kuaishou" data-sync-origin="owned"/);
|
||||
assert.match(owned, /去找对标看快照/);
|
||||
|
||||
assert.match(modelOverview, /语言模型缺口/);
|
||||
assert.match(modelOverview, /ASR 缺口/);
|
||||
assert.match(modelOverview, /文生图缺口/);
|
||||
assert.match(modelOverview, /图生图缺口/);
|
||||
assert.match(modelOverview, /生视频缺口/);
|
||||
assert.match(modelOverview, /open-admin-system-model/);
|
||||
assert.match(modelOverview, /open-admin-runtime-config/);
|
||||
assert.match(modelOverview, /open-admin-huobao-ai-config/);
|
||||
assert.match(modelOverview, /data-service-type="image"/);
|
||||
assert.match(modelOverview, /data-service-type="video"/);
|
||||
});
|
||||
|
||||
test("ai video and huobao admin config expose preflight and connection test flows", () => {
|
||||
const guardSource = extractBetween(APP, "function getPipelineGuard(kind) {", "function getIntegrationCards()");
|
||||
const preflightSource = extractBetween(APP, "function getAiVideoProviderPreflight(provider = \"doubao\", model = \"\") {", "function renderAiVideoProviderHintHtml(");
|
||||
const aiVideoSource = extractBetween(APP, "function openCreateAiVideoAction(defaults = {})", "function openCreateRealCutAction(");
|
||||
const huobaoSource = extractBetween(APP, "async function openAdminHuobaoConfigAction(serviceType = \"video\", configId = \"\") {", "function openAdminHuobaoConfigDeleteAction(");
|
||||
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
|
||||
assert.match(guardSource, /detail\.videoConfigReady === false/);
|
||||
assert.match(guardSource, /detail\.videoConfigCount <= 0/);
|
||||
assert.match(preflightSource, /Huobao 视频配置未就绪,请先在管理后台完成视频配置/);
|
||||
assert.match(preflightSource, /请先在 Huobao 启用至少一条视频配置/);
|
||||
assert.match(preflightSource, /Huobao 启用中的视频配置未包含所选 Seedance 模型/);
|
||||
assert.match(preflightSource, /所选 Seedance 模型/);
|
||||
assert.match(preflightSource, /seedance-2\.0-pro/);
|
||||
assert.match(aiVideoSource, /const videoPreflight = getAiVideoProviderPreflight\(normalizedProvider, normalizedVideoModel\)/);
|
||||
assert.match(aiVideoSource, /if \(!videoPreflight\.ready\) throw new Error\(videoPreflight\.reason\);/);
|
||||
assert.match(aiVideoSource, /renderAiVideoProviderHintHtml\(defaultVideoProvider, defaultVideoModel\)/);
|
||||
assert.match(APP, /renderAiVideoProviderHintHtml\(provider, modelInput\?\.value \|\| seedanceModel\)/);
|
||||
assert.match(aiVideoSource, /videoPreflight\.reason/);
|
||||
assert.match(huobaoSource, /submitLabel: existing \? "保存配置" : "创建配置并测试"/);
|
||||
assert.match(huobaoSource, /testLabel:\s*"测试配置"/);
|
||||
assert.match(huobaoSource, /const testPayload = await storyforgeFetch\("\/v2\/admin\/model-access\/huobao-configs\/test"/);
|
||||
assert.match(huobaoSource, /keepOpen: true/);
|
||||
assert.match(huobaoSource, /测试通过/);
|
||||
assert.match(clickActions, /name === "test-sheet"[\s\S]*submitActionModal\("onTest"/);
|
||||
});
|
||||
|
||||
test("governance and quota panels use real empty-state language instead of backend-sync placeholders", () => {
|
||||
const actionRegistry = extractBetween(APP, "function renderOneLinerActionRegistryPanel()", "function renderTenantQuotaPanel()");
|
||||
const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel(");
|
||||
@@ -448,10 +507,12 @@ test("job detail and follow-up flows use direct generate-copy execution and pers
|
||||
});
|
||||
|
||||
test("ai video provider hint links super admins into the huobao video config workspace", () => {
|
||||
const hintSource = extractBetween(APP, "function renderAiVideoProviderHintHtml(provider = \"doubao\") {", "function renderAiVideoProviderMemoryHtml(");
|
||||
const hintSource = extractBetween(APP, "function renderAiVideoProviderHintHtml(provider = \"doubao\", model = \"\") {", "function renderAiVideoProviderMemoryHtml(");
|
||||
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
assert.match(hintSource, /打开视频引擎配置/);
|
||||
assert.match(hintSource, /focus-huobao-video-config/);
|
||||
assert.match(hintSource, /当前模型/);
|
||||
assert.match(hintSource, /配置中的模型/);
|
||||
assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAdminModelAccessWorkspace\("admin-model-video-anchor"\)/);
|
||||
});
|
||||
|
||||
@@ -552,6 +613,17 @@ test("production queue promotes intake entrypoints so import flows are reachable
|
||||
assert.match(mobileDeck, /actionTag\("视频录制", "focus-live-recorder-maintenance"\)/);
|
||||
});
|
||||
|
||||
test("discovery and production promote creator-center sync entrypoints for douyin and kuaishou", () => {
|
||||
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
|
||||
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
|
||||
assert.match(discovery, /button\("登录抖音创作者中心", "open-creator-center-sync"/);
|
||||
assert.match(discovery, /button\("登录快手创作者中心", "open-creator-center-sync"/);
|
||||
assert.match(discovery, /actionTag\("接入抖音创作者中心", "open-creator-center-sync"/);
|
||||
assert.match(production, /button\("接入抖音创作者中心", "open-creator-center-sync"/);
|
||||
assert.match(production, /button\("接入快手创作者中心", "open-creator-center-sync"/);
|
||||
assert.match(production, /actionTag\("接入抖音创作者中心", "open-creator-center-sync"/);
|
||||
});
|
||||
|
||||
test("discovery page promotes selected-account actions into direct execute flows", () => {
|
||||
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
|
||||
const discoveryOverview = extractBetween(APP, "function renderDiscoveryOverviewSection(", "function renderDiscoveryRelationsSection(");
|
||||
@@ -577,6 +649,46 @@ test("direct discovery analysis actions gracefully fall back to forms when no ac
|
||||
assert.match(APP, /if \(name === "direct-analyze-top-videos"\)[\s\S]*openAnalyzeTopVideosAction\(\);/);
|
||||
});
|
||||
|
||||
test("creator center sync modal covers douyin and kuaishou creator-center login flows", () => {
|
||||
const clickActions = extractBetween(APP, 'document.addEventListener("click", async (event) => {', 'navButtons.forEach((button) => {');
|
||||
const creatorCenterSync = extractBetween(APP, "function openCreatorCenterSyncAction(defaults = {}) {", "function openImportVideoLinkAction()");
|
||||
assert.match(APP, /function openCreatorCenterSyncAction\(defaults = \{\}\)/);
|
||||
assert.match(APP, /title: "\$\{platformLabel\(platform\)\}创作者中心接入"/);
|
||||
assert.match(APP, /name: "sessionCookie", label: "登录 Cookie", type: "password"/);
|
||||
assert.match(APP, /name: "creatorCenterUrls", label: "创作者中心页面"/);
|
||||
assert.match(APP, /name: "autoAnalyze", label: "同步后自动分析", type: "checkbox", value: true/);
|
||||
assert.match(APP, /const syncPath = `\/v2\/\$\{platform\}\/accounts\/sync`/);
|
||||
assert.match(APP, /const analyzePath = getWorkbenchRoute\(platform, "analyzeAccount", workspace\?\.account\?\.id \|\| ""\)/);
|
||||
assert.match(creatorCenterSync, /auto_analyze_top_videos: true/);
|
||||
assert.match(creatorCenterSync, /const topVideoAnalyses = safeArray\(analyzeResult\?\.top_video_analyses\)/);
|
||||
assert.match(creatorCenterSync, /appState\.topVideoAnalysisResults = \{/);
|
||||
assert.match(APP, /manual_creator_pages: \[\]/);
|
||||
assert.match(APP, /await loadPlatformAccount\(platform, workspace\.account\.id\)/);
|
||||
assert.match(creatorCenterSync, /focusDiscoveryTopVideoInsights\(\)/);
|
||||
assert.match(APP, /focusDiscoveryInsights\(\)/);
|
||||
assert.match(clickActions, /name === "open-creator-center-sync"[\s\S]*openCreatorCenterSyncAction\(\{/);
|
||||
});
|
||||
|
||||
test("workbench account loading fetches snapshots and creator fields for all supported platforms", () => {
|
||||
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
|
||||
const snapshotDetailAction = extractBetween(APP, "async function openDouyinSnapshotDetailAction(", "function renderLiveRecorderManagementPanel()");
|
||||
assert.match(loadPlatformAccountSource, /const snapshotsPath = getWorkbenchRoute\(normalizedPlatform, "snapshots", accountId\);/);
|
||||
assert.match(loadPlatformAccountSource, /const analysisReportsPath = getWorkbenchRoute\(normalizedPlatform, "analysisReports", accountId\);/);
|
||||
assert.match(loadPlatformAccountSource, /const creatorFieldsPath = getWorkbenchRoute\(normalizedPlatform, "creatorFields", accountId\);/);
|
||||
assert.match(loadPlatformAccountSource, /const snapshotDetailPath = getWorkbenchRoute\(normalizedPlatform, "snapshotDetail", accountId, nextSnapshotId\);/);
|
||||
assert.doesNotMatch(loadPlatformAccountSource, /normalizedPlatform === "douyin"/);
|
||||
assert.match(APP, /function renderCreatorInsightPanel\(\)/);
|
||||
assert.match(snapshotDetailAction, /const detailPath = getWorkbenchRoute\(platform, "snapshotDetail", selected\.id, snapshotId\);/);
|
||||
});
|
||||
|
||||
test("creator insight panel surfaces report model and linked-account context", () => {
|
||||
const insightPanel = extractBetween(APP, "function renderCreatorInsightPanel() {", "function renderDouyinInsightPanel() {");
|
||||
assert.match(insightPanel, /report\.model_profile_ids\?\.length/);
|
||||
assert.match(insightPanel, /report\.linked_account_ids\?\.length/);
|
||||
assert.match(insightPanel, /已绑对标/);
|
||||
assert.match(insightPanel, /模型 /);
|
||||
});
|
||||
|
||||
test("mobile discovery prioritizes the selected-account task flow before the scrollable account list", () => {
|
||||
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
|
||||
assert.match(discovery, /mobile-discovery-priority/);
|
||||
@@ -1179,7 +1291,7 @@ test("ai video form explains where Seedance 火山配置 lives", () => {
|
||||
'document.addEventListener("click", async (event) => {',
|
||||
'navButtons.forEach((button) => {'
|
||||
);
|
||||
assert.match(APP, /function renderAiVideoProviderHintHtml\(provider = "doubao"\)/);
|
||||
assert.match(APP, /function renderAiVideoProviderHintHtml\(provider = "doubao", model = ""\)/);
|
||||
assert.match(APP, /Seedance 2\.0 走火山视频配置/);
|
||||
assert.match(APP, /\/settings\/ai-config/);
|
||||
assert.match(APP, /视频 -> 火山引擎/);
|
||||
@@ -1197,6 +1309,8 @@ test("ai video form explains where Seedance 火山配置 lives", () => {
|
||||
assert.match(APP, /当前项目最近视频引擎/);
|
||||
assert.match(APP, /上次创建 AI 视频时使用的是/);
|
||||
assert.match(APP, /modelInput\.placeholder = provider === "seedance2" \? "例如:seedance-2\.0-pro" : "留空则沿用当前默认视频模型"/);
|
||||
assert.match(APP, /modelInput\.addEventListener\("input", \(\) => \{/);
|
||||
assert.match(APP, /hintHtml\.innerHTML = renderAiVideoProviderHintHtml\(provider, modelInput\?\.value \|\| seedanceModel\)/);
|
||||
assert.match(APP, /bindAiVideoProviderRecommendations\(fields, \{ seedanceModel: defaultVideoModel \|\| "seedance-2\.0-pro" \}\)/);
|
||||
assert.match(APP, /saveAiVideoPreferences\(project\.id, \{/);
|
||||
assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAutomationHealthWorkspace\("integration-huobao-anchor"\)/);
|
||||
@@ -1424,11 +1538,57 @@ test("discovery analysis actions focus the most relevant detail tab after succes
|
||||
assert.match(APP, /function focusDiscoveryInsights\(\)/);
|
||||
assert.match(APP, /function focusDiscoveryTopVideoInsights\(\)/);
|
||||
assert.match(APP, /function focusDiscoveryRelations\(\)/);
|
||||
assert.match(analyzeAccount, /safeArray\(result\.top_video_analyses\)\.length/);
|
||||
assert.match(analyzeAccount, /appState\.topVideoAnalysisResults = \{/);
|
||||
assert.match(analyzeAccount, /focusDiscoveryTopVideoInsights\(\)/);
|
||||
assert.match(analyzeAccount, /focusDiscoveryInsights\(\)/);
|
||||
assert.match(analyzeTopVideos, /focusDiscoveryTopVideoInsights\(\)/);
|
||||
assert.match(similaritySearch, /focusDiscoveryRelations\(\)/);
|
||||
});
|
||||
|
||||
test("discovery relations section surfaces candidate provenance and overlap context", () => {
|
||||
const relations = extractBetween(APP, "function renderDiscoveryRelationsSection(linkedAccounts, similarCandidates) {", "function renderDiscoveryScreen()");
|
||||
assert.match(relations, /const searchContext = appState\.lastSimilaritySearch\?\.context \|\| \{\}/);
|
||||
assert.match(relations, /const dimensions = candidate\.dimensions \|\| candidate\.dimensions_json \|\| \{\}/);
|
||||
assert.match(relations, /dimensions\.source === "linked_account"/);
|
||||
assert.match(relations, /dimensions\.source === "manual_url"/);
|
||||
assert.match(relations, /source_overlap != null/);
|
||||
assert.match(relations, /requirement_overlap != null/);
|
||||
assert.match(relations, /最近这一轮会优先合并已绑关系、手动主页和本地账号池/);
|
||||
});
|
||||
|
||||
test("similarity search state is isolated per selected account", () => {
|
||||
const state = extractBetween(APP, "const appState = {", "};\n\nlet PLATFORM_RUNTIME");
|
||||
const hydrateSource = extractBetween(APP, "async function hydrateWorkbenchDataAfterBootstrap(", "async function bootstrap()");
|
||||
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
|
||||
const markSavedCandidate = extractBetween(APP, "function markSavedCandidate(candidate, links) {", "async function saveCandidateAsBenchmark(candidateIndex, relationType = \"benchmark\") {");
|
||||
const similaritySearch = extractBetween(APP, "function openSimilaritySearchAction()", "function openBenchmarkLinkAction(defaults = {})");
|
||||
assert.match(state, /similaritySearchResultsByAccount: \{\}/);
|
||||
assert.match(hydrateSource, /if \(!nextAccountId\) \{[\s\S]*appState\.lastSimilaritySearch = null;/);
|
||||
assert.match(loadPlatformAccountSource, /appState\.lastSimilaritySearch = appState\.similaritySearchResultsByAccount\?\.\[accountId\] \|\| null;/);
|
||||
assert.doesNotMatch(loadPlatformAccountSource, /appState\.similarSearchDetail/);
|
||||
assert.match(markSavedCandidate, /appState\.similaritySearchResultsByAccount = \{/);
|
||||
assert.match(similaritySearch, /appState\.similaritySearchResultsByAccount = \{/);
|
||||
assert.match(similaritySearch, /\[account\.id\]: detail/);
|
||||
});
|
||||
|
||||
test("logout clears account-scoped discovery caches", () => {
|
||||
const logoutSource = extractBetween(APP, "async function logoutSession() {", "async function loadKnowledgeDocuments(");
|
||||
assert.match(logoutSource, /appState\.lastSimilaritySearch = null;/);
|
||||
assert.match(logoutSource, /appState\.similaritySearchResultsByAccount = \{\};/);
|
||||
assert.match(logoutSource, /appState\.topVideoAnalysisResults = \{\};/);
|
||||
});
|
||||
|
||||
test("account loading clears stale workbench detail before surfacing fetch failures", () => {
|
||||
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
|
||||
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.lastSimilaritySearch = null;/);
|
||||
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.selectedWorkspace = null;/);
|
||||
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.selectedVideos = \{ items: \[], meta: \{}, top_scored_video_ids: \[], latest_video_ids: \[], high_score_threshold: 60 \};/);
|
||||
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.snapshots = \[];/);
|
||||
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.analysisReports = \[];/);
|
||||
assert.match(loadPlatformAccountSource, /throw error;/);
|
||||
});
|
||||
|
||||
test("tracking and benchmark actions land on the most relevant workbench area after success", () => {
|
||||
const trackSelected = extractBetween(APP, "function openTrackSelectedAccountAction()", "function openImportVideoLinkAction()");
|
||||
const saveCandidate = extractBetween(APP, "async function saveCandidateAsBenchmark(candidateIndex, relationType = \"benchmark\")", "function screenShell(title, subtitle, actionsHtml, bodyHtml)");
|
||||
|
||||
Reference in New Issue
Block a user