test: clean stale E2E temp dirs before runs and remove stray media capture test

This commit is contained in:
hy committed 2026-08-04 13:23:19 +08:00
1 parent f50f4ebbdb
commit 2b853c70e2
3 files changed
+33 -246

No files matched your search

+2 -1
View File
@@ -139,7 +139,8 @@ runner 的职责:
1. 找到目标测试文件并执行。
2. 落原始日志到 `tests/logs/**`。
3. 在不带 `--type` 时按“普通测试 -> 源码 E2E”顺序执行。
4. 按 `--target` 准备源码应用,或构建、审计并安装正式包。
4. E2E 执行前清理 `tests/.tmp/e2e` 中超过 24 小时未活动的残留 `ncurator-*` 目录,避免异常终止的 run 遗留临时数据。
5. 按 `--target` 准备源码应用,或构建、审计并安装正式包。
runner 不负责:
+31
View File
@@ -33,6 +33,7 @@ const LIVE_WORKERS = 3
const LIVE_BACKEND_RETRIES = 1
const RUNNER_DEADLINE_MS = 55 * 60 * 1000
const DEADLINE_EXIT_CODE = 124
const STALE_E2E_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000
const E2E_EXECUTION_CLASSES = new Set(['normal', 'live', 'exclusive', 'warmup'])
const CROSS_ARCH_WARMUP_SOURCE_TARGET = new Map([['macos-x64', 'macos-arm64']])
const USE_WINDOWS_SHELL = process.platform === 'win32'
@@ -857,6 +858,30 @@ const runE2ETestFilesConcurrently = async (testFiles, targets, testNamePattern,
}
}
const cleanupStaleE2ETempRoot = async (e2eTempRoot) => {
let entries = []
try {
entries = await readdir(e2eTempRoot, { withFileTypes: true })
} catch {
return 0
}
const now = Date.now()
let removedCount = 0
for (const entry of entries) {
if (!entry.isDirectory() || !entry.name.startsWith('ncurator-')) continue
const entryPath = path.join(e2eTempRoot, entry.name)
try {
const entryStat = await stat(entryPath)
if (now - entryStat.mtimeMs <= STALE_E2E_TEMP_MAX_AGE_MS) continue
await rm(entryPath, { recursive: true, force: true })
removedCount += 1
} catch {
// 单个残留目录清理失败不阻断本次 run
}
}
return removedCount
}
const runTypeWorkflow = async (input) => {
const testFiles = await findTestFiles(input)
if (testFiles.length === 0) {
@@ -887,6 +912,12 @@ const runTypeWorkflow = async (input) => {
path.join(TESTS_ROOT, '.tmp', 'e2e')
)
await mkdir(e2eTempRoot, { recursive: true })
const removedTempCount = await cleanupStaleE2ETempRoot(e2eTempRoot)
if (removedTempCount > 0) {
process.stdout.write(
`[test-workflow] cleaned ${removedTempCount} stale E2E temp dir(s) in ${e2eTempRoot}\n`
)
}
runtimeEnv = {
...runtimeEnv,
NCURATOR_E2E_TEMP_ROOT: e2eTempRoot
@@ -1,245 +0,0 @@
import { cp, mkdir, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { launchElectronApp } from '../harness/e2e/electronApp'
import {
configureLiveModelThroughSettingsUi,
getWorkbenchLocator,
getWorkbenchPrefixLocator,
loadWorkbenchLiveModelEnv,
navigateToWorkbench,
prepareSelectedWorkspaceFilesThroughSettingsUi,
selectWorkbenchModel,
selectWorkbenchWorkspace,
workbenchSelectors
} from '../harness/e2e/workbench'
import {
bindWorkspaceRootThroughIpc,
createWorkspaceThroughSettingsUi,
navigateToSettingsSection
} from '../harness/e2e/settings'
const OUTPUT_ROOT = '/Users/test/work-space/ncurator-media-preview/real-capture'
const SOURCE_WORKSPACE = '/Users/test/work-space/ncurator-media-preview/demo-workspace'
const WORKSPACE_ROOT = path.join(OUTPUT_ROOT, 'workspace-run')
const WORKSPACE_NAME = '明远科技客户会议'
const PROMPT =
'下午 3 点要和明远科技开项目复盘会。请根据会议纪要、需求确认单、交付记录和客户反馈,整理一份会前简报,包含客户最关心的 3 件事、已完成事项、当前风险,以及会上需要确认的 5 个问题。请生成《明远科技会前简报.docx》,并在结论后标出依据。'
const frameName = (index: number): string => `frame-${String(index).padStart(5, '0')}.jpg`
describe('official media real product capture', () => {
it(
'captures the actual product running the customer meeting task',
async () => {
await rm(OUTPUT_ROOT, { recursive: true, force: true })
await mkdir(path.join(OUTPUT_ROOT, 'raw', 'typing'), { recursive: true })
await mkdir(path.join(OUTPUT_ROOT, 'raw', 'run'), { recursive: true })
await mkdir(path.join(OUTPUT_ROOT, 'raw', 'answer'), { recursive: true })
await mkdir(path.join(OUTPUT_ROOT, 'raw', 'docx'), { recursive: true })
await mkdir(path.join(OUTPUT_ROOT, 'screenshots'), { recursive: true })
await cp(SOURCE_WORKSPACE, WORKSPACE_ROOT, { recursive: true, force: true })
await rm(path.join(WORKSPACE_ROOT, '明远科技会前简报.docx'), { force: true })
const env = loadWorkbenchLiveModelEnv()
const launched = await launchElectronApp({ launchMode: 'real', windowMode: 'background' })
const { electronApp, page } = launched
const tracePath = path.join(OUTPUT_ROOT, 'product-run-trace.zip')
const timestamps: Array<{ section: string; index: number; at: number }> = []
try {
await electronApp.evaluate(({ BrowserWindow }) => {
const window = BrowserWindow.getAllWindows()[0]
window?.setContentSize(1600, 1000)
})
await page.setViewportSize({ width: 1600, height: 1000 })
await configureLiveModelThroughSettingsUi(page, env)
await navigateToSettingsSection(page, 'model')
const liveRow = page.locator('tbody tr').filter({ hasText: 'Live Model' }).first()
await liveRow.waitFor({ state: 'visible' })
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'screenshots', 'feature-cost-real.png'),
type: 'png'
})
await navigateToSettingsSection(page, 'workspace')
const workspaceId = await createWorkspaceThroughSettingsUi(page, WORKSPACE_NAME)
await bindWorkspaceRootThroughIpc(page, WORKSPACE_ROOT)
await navigateToSettingsSection(page, 'model')
await navigateToSettingsSection(page, 'workspace')
await prepareSelectedWorkspaceFilesThroughSettingsUi(page, workspaceId)
await navigateToWorkbench(page, { waitForSessionListText: false })
await selectWorkbenchWorkspace(page, WORKSPACE_NAME)
const newConversation = page.getByRole('button', {
name: `在 ${WORKSPACE_NAME} 中新建对话`
})
if (await newConversation.count()) {
await newConversation.click()
}
await selectWorkbenchWorkspace(page, WORKSPACE_NAME)
await selectWorkbenchModel(page, env.modelName)
const input = getWorkbenchLocator(page, workbenchSelectors.input)
await input.click()
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'raw', 'typing', frameName(0)),
type: 'jpeg',
quality: 94
})
let typingIndex = 1
for (let offset = 0; offset < PROMPT.length; offset += 4) {
const chunk = PROMPT.slice(offset, offset + 4)
await page.keyboard.insertText(chunk)
await page.waitForTimeout(22)
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'raw', 'typing', frameName(typingIndex)),
type: 'jpeg',
quality: 94
})
timestamps.push({ section: 'typing', index: typingIndex, at: Date.now() })
typingIndex += 1
}
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'screenshots', 'task-ready-real.png'),
type: 'png'
})
const startedAt = Date.now()
await getWorkbenchLocator(page, workbenchSelectors.sendButton).click()
let runIndex = 0
let completed = false
let failure = ''
const decidedApprovalBatches = new Set<string>()
while (!completed && !failure && Date.now() - startedAt < 900_000) {
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'raw', 'run', frameName(runIndex)),
type: 'jpeg',
quality: 92
})
timestamps.push({ section: 'run', index: runIndex, at: Date.now() })
runIndex += 1
const signals = await launched.collectSignals()
const events = signals.aiRunEventEntries.map((entry) => entry.event)
const failed = events.find((event) => event.type === 'run_failed')
if (failed?.type === 'run_failed') {
failure = failed.error
break
}
const approval = events
.filter((event) => event.type === 'approval_batch_required')
.at(-1)
if (
approval?.type === 'approval_batch_required' &&
!decidedApprovalBatches.has(approval.batchId)
) {
const approveButton = page.locator(
'[data-testid="chat-approval-batch-approve-button"]'
)
for (let item = 0; item < approval.items.length; item += 1) {
await approveButton.waitFor({ state: 'visible' })
await approveButton.click()
await page.waitForTimeout(350)
}
decidedApprovalBatches.add(approval.batchId)
}
completed = events.some((event) => event.type === 'run_completed')
await page.waitForTimeout(120)
}
expect(failure).toBe('')
expect(completed).toBe(true)
await page.waitForTimeout(800)
const finalAnswer = getWorkbenchPrefixLocator(
page,
workbenchSelectors.assistantFinalAnswerPrefix
).last()
await finalAnswer.waitFor({ state: 'visible' })
await finalAnswer.scrollIntoViewIfNeeded()
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'screenshots', 'feature-knowledge-real.png'),
type: 'png'
})
for (let index = 0; index < 36; index += 1) {
if (index > 11 && index % 3 === 0) {
await getWorkbenchLocator(page, workbenchSelectors.messageList).evaluate((element) => {
element.scrollTop += 90
})
}
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'raw', 'answer', frameName(index)),
type: 'jpeg',
quality: 94
})
await page.waitForTimeout(65)
}
const applyEntry = getWorkbenchPrefixLocator(
page,
workbenchSelectors.assistantTurnApplyPrefix
).last()
await applyEntry.waitFor({ state: 'visible' })
await applyEntry.scrollIntoViewIfNeeded()
await applyEntry.click()
await getWorkbenchLocator(page, workbenchSelectors.applyPanel).waitFor({ state: 'visible' })
await getWorkbenchLocator(page, workbenchSelectors.documentView).waitFor({
state: 'visible'
})
await page.waitForTimeout(1400)
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'screenshots', 'feature-autonomous-real.png'),
type: 'png'
})
for (let index = 0; index < 42; index += 1) {
if (index > 12 && index % 3 === 0) {
await getWorkbenchLocator(page, workbenchSelectors.documentView).evaluate((element) => {
const scrollHost =
Array.from(element.querySelectorAll('*')).find(
(node) =>
node instanceof HTMLElement &&
node.scrollHeight > node.clientHeight + 40 &&
getComputedStyle(node).overflowY !== 'visible'
) || element
if (scrollHost instanceof HTMLElement) scrollHost.scrollTop += 100
})
}
await page.screenshot({
path: path.join(OUTPUT_ROOT, 'raw', 'docx', frameName(index)),
type: 'jpeg',
quality: 94
})
await page.waitForTimeout(65)
}
await writeFile(
path.join(OUTPUT_ROOT, 'capture-manifest.json'),
JSON.stringify(
{
prompt: PROMPT,
workspaceName: WORKSPACE_NAME,
modelName: env.modelName,
typingFrameCount: typingIndex,
runFrameCount: runIndex,
timestamps
},
null,
2
),
'utf8'
)
} finally {
await launched.close({ tracePath })
}
},
1_200_000
)
})