@web-auto/webauto 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop-console/default-settings.json +2 -2
- package/apps/desktop-console/dist/main/index.mjs +983 -128
- package/apps/desktop-console/dist/main/preload.mjs +7 -0
- package/apps/desktop-console/dist/renderer/index.html +622 -50
- package/apps/desktop-console/dist/renderer/index.js +2423 -469
- package/apps/desktop-console/dist/renderer/run.mts +6 -5
- package/apps/desktop-console/entry/ui-cli.mjs +672 -0
- package/apps/desktop-console/entry/ui-console.mjs +416 -29
- package/apps/webauto/entry/account.mjs +89 -53
- package/apps/webauto/entry/browser-status.mjs +7 -10
- package/apps/webauto/entry/lib/account-detect.mjs +254 -28
- package/apps/webauto/entry/lib/account-store.mjs +219 -30
- package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
- package/apps/webauto/entry/lib/profilepool.mjs +14 -5
- package/apps/webauto/entry/lib/quota-status.mjs +23 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
- package/apps/webauto/entry/profilepool.mjs +106 -17
- package/apps/webauto/entry/schedule.mjs +612 -0
- package/apps/webauto/entry/weibo-unified.mjs +134 -0
- package/apps/webauto/entry/xhs-install.mjs +256 -31
- package/apps/webauto/entry/xhs-status.mjs +5 -2
- package/apps/webauto/entry/xhs-unified.mjs +631 -98
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
- package/bin/camoufox-cli.mjs +61 -0
- package/bin/webauto.mjs +301 -54
- package/dist/modules/camo-backend/src/index.js +49 -1
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
- package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
- package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
- package/dist/modules/collection-manager/bloom-filter.js +91 -0
- package/dist/modules/collection-manager/date-utils.js +275 -0
- package/dist/modules/collection-manager/index.js +258 -0
- package/dist/modules/collection-manager/storage.js +195 -0
- package/dist/modules/collection-manager/types.js +47 -0
- package/dist/modules/logging/src/index.js +1 -1
- package/dist/modules/process-registry/index.js +230 -0
- package/dist/modules/rate-limiter/index.js +242 -0
- package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
- package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
- package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
- package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
- package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
- package/dist/modules/workflow/config/workflowRegistry.js +2 -0
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
- package/dist/modules/workflow/src/runner.js +6 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/shared/serviceProcessLogger.js +1 -1
- package/dist/services/unified-api/server.js +105 -11
- package/modules/camo-backend/src/index.ts +46 -1
- package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
- package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
- package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
- package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
- package/modules/collection-manager/bloom-filter.ts +112 -0
- package/modules/collection-manager/date-utils.ts +316 -0
- package/modules/collection-manager/index.ts +309 -0
- package/modules/collection-manager/package.json +10 -0
- package/modules/collection-manager/storage.ts +174 -0
- package/modules/collection-manager/types.ts +156 -0
- package/modules/logging/src/index.ts +1 -1
- package/modules/process-registry/index.ts +284 -0
- package/modules/rate-limiter/index.ts +322 -0
- package/modules/state/src/paths.ts +9 -1
- package/modules/task-scheduler/index.ts +293 -0
- package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
- package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
- package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
- package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
- package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
- package/modules/workflow/config/workflowRegistry.ts +2 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
- package/modules/workflow/src/runner.ts +6 -0
- package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
- package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
- package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
- package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
- package/package.json +13 -4
- package/scripts/postinstall-resources.mjs +62 -0
- package/scripts/test/run-coverage.mjs +76 -0
- package/scripts/weibo/search.ts +49 -0
- package/services/shared/serviceProcessLogger.ts +1 -1
- package/services/unified-api/server.ts +98 -12
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import minimist from 'minimist';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, statSync, rmSync } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const APP_ROOT = path.resolve(__dirname, '
|
|
10
|
+
const APP_ROOT = path.resolve(__dirname, '..');
|
|
10
11
|
const DIST_MAIN = path.join(APP_ROOT, 'dist', 'main', 'index.mjs');
|
|
11
12
|
|
|
12
13
|
const args = minimist(process.argv.slice(2), {
|
|
13
|
-
boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon'],
|
|
14
|
-
string: ['profile', 'keyword', 'target', 'scenario', 'output'],
|
|
14
|
+
boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon', 'dry-run', 'no-dry-run', 'parallel', 'do-likes'],
|
|
15
|
+
string: ['profile', 'profiles', 'keyword', 'target', 'scenario', 'output', 'concurrency', 'like-keywords', 'max-likes'],
|
|
15
16
|
alias: { h: 'help', p: 'profile', k: 'keyword', t: 'target', o: 'output' }
|
|
16
17
|
});
|
|
17
18
|
|
|
@@ -27,6 +28,7 @@ Test Scenarios:
|
|
|
27
28
|
account-flow - Account creation/login flow test
|
|
28
29
|
config-save - Config save/load test
|
|
29
30
|
crawl-run - Full crawl flow test
|
|
31
|
+
full-cover - Real end-to-end UI control + status coverage (no mock)
|
|
30
32
|
|
|
31
33
|
Options:
|
|
32
34
|
--check Check build/dep status only
|
|
@@ -35,13 +37,54 @@ Options:
|
|
|
35
37
|
--no-daemon Run in foreground mode
|
|
36
38
|
--scenario Test scenario name
|
|
37
39
|
--profile Test profile ID
|
|
40
|
+
--profiles Test profile IDs (comma-separated)
|
|
38
41
|
--keyword Test keyword
|
|
39
42
|
--target Target count
|
|
43
|
+
--like-keywords Like keyword filter
|
|
44
|
+
--do-likes Enable likes
|
|
45
|
+
--max-likes Max likes per note (0=unlimited)
|
|
46
|
+
--parallel Enable parallel sharding
|
|
47
|
+
--concurrency Parallel concurrency
|
|
40
48
|
--headless Headless mode
|
|
41
49
|
--output Output report path
|
|
42
50
|
`);
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
function resolveDownloadRoot() {
|
|
54
|
+
const fromEnv = String(process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR || '').trim();
|
|
55
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
56
|
+
if (process.platform === 'win32') {
|
|
57
|
+
try {
|
|
58
|
+
if (existsSync('D:\\')) return 'D:\\webauto';
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
return path.join(os.homedir(), '.webauto');
|
|
63
|
+
}
|
|
64
|
+
return path.join(os.homedir(), '.webauto', 'download');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findLatestSummary(keyword) {
|
|
68
|
+
const root = resolveDownloadRoot();
|
|
69
|
+
const safeKeyword = String(keyword || '').trim();
|
|
70
|
+
if (!safeKeyword) return null;
|
|
71
|
+
const keywordDir = path.join(root, 'xiaohongshu', 'debug', safeKeyword);
|
|
72
|
+
const mergedDir = path.join(keywordDir, 'merged');
|
|
73
|
+
if (!existsSync(mergedDir)) return null;
|
|
74
|
+
const entries = readdirSync(mergedDir, { withFileTypes: true })
|
|
75
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('run-'))
|
|
76
|
+
.map((e) => {
|
|
77
|
+
const full = path.join(mergedDir, e.name);
|
|
78
|
+
return { full, mtime: statSync(full).mtimeMs };
|
|
79
|
+
})
|
|
80
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const summaryPath = path.join(entry.full, 'summary.json');
|
|
83
|
+
if (existsSync(summaryPath)) return summaryPath;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
45
88
|
function checkBuildStatus() {
|
|
46
89
|
return existsSync(DIST_MAIN);
|
|
47
90
|
}
|
|
@@ -83,14 +126,23 @@ async function startConsole(noDaemon = false) {
|
|
|
83
126
|
}
|
|
84
127
|
|
|
85
128
|
console.log('[ui-console] Starting Desktop Console...');
|
|
129
|
+
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
86
130
|
const env = { ...process.env };
|
|
87
131
|
if (noDaemon) env.WEBAUTO_NO_DAEMON = '1';
|
|
132
|
+
const detached = !noDaemon;
|
|
133
|
+
const stdio = detached ? 'ignore' : 'inherit';
|
|
88
134
|
|
|
89
|
-
const
|
|
135
|
+
const useCmd = process.platform === 'win32';
|
|
136
|
+
const spawnCmd = useCmd ? 'cmd.exe' : npxBin;
|
|
137
|
+
const spawnArgs = useCmd
|
|
138
|
+
? ['/d', '/s', '/c', npxBin, 'electron', DIST_MAIN]
|
|
139
|
+
: ['electron', DIST_MAIN];
|
|
140
|
+
|
|
141
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
90
142
|
cwd: APP_ROOT,
|
|
91
143
|
env,
|
|
92
|
-
stdio
|
|
93
|
-
detached
|
|
144
|
+
stdio,
|
|
145
|
+
detached
|
|
94
146
|
});
|
|
95
147
|
|
|
96
148
|
if (noDaemon) {
|
|
@@ -122,30 +174,101 @@ class UITestRunner {
|
|
|
122
174
|
this.results.push({ ts, type, message });
|
|
123
175
|
}
|
|
124
176
|
|
|
125
|
-
async runCommand(cmd, args, timeoutMs = 30000) {
|
|
177
|
+
async runCommand(cmd, args, timeoutMs = 30000, options = {}) {
|
|
126
178
|
return new Promise((resolve, reject) => {
|
|
127
|
-
const
|
|
179
|
+
const env = { ...process.env, ...(options.env || {}) };
|
|
180
|
+
const child = spawn(cmd, args, { shell: false, stdio: 'pipe', cwd: options.cwd || process.cwd(), env });
|
|
128
181
|
let stdout = '';
|
|
129
182
|
let stderr = '';
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
183
|
+
let timer = null;
|
|
184
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
185
|
+
timer = setTimeout(() => {
|
|
186
|
+
child.kill('SIGTERM');
|
|
187
|
+
reject(new Error(`Command timeout: ${cmd}`));
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
}
|
|
134
190
|
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
135
191
|
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
136
192
|
child.on('close', (code) => {
|
|
137
|
-
clearTimeout(timer);
|
|
193
|
+
if (timer) clearTimeout(timer);
|
|
138
194
|
if (code === 0) resolve({ ok: true, stdout, stderr });
|
|
139
195
|
else reject(new Error(stderr || `Exit code: ${code}`));
|
|
140
196
|
});
|
|
141
197
|
});
|
|
142
198
|
}
|
|
143
199
|
|
|
200
|
+
ensure(condition, message) {
|
|
201
|
+
if (!condition) throw new Error(message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
parseJsonOutput(output) {
|
|
205
|
+
const text = String(output || '').trim();
|
|
206
|
+
if (!text) return null;
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(text);
|
|
209
|
+
} catch {
|
|
210
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
211
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(lines[i]);
|
|
214
|
+
} catch {
|
|
215
|
+
// keep scanning
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async runNodeScript(scriptPath, scriptArgs = [], timeoutMs = 30000, options = {}) {
|
|
223
|
+
const cmd = process.execPath;
|
|
224
|
+
return this.runCommand(cmd, [scriptPath, ...scriptArgs], timeoutMs, options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async runJsonNodeScript(scriptPath, scriptArgs = [], timeoutMs = 30000, options = {}) {
|
|
228
|
+
const out = await this.runNodeScript(scriptPath, scriptArgs, timeoutMs, options);
|
|
229
|
+
const json = this.parseJsonOutput(out.stdout);
|
|
230
|
+
this.ensure(json && typeof json === 'object', `JSON parse failed for ${path.basename(scriptPath)}`);
|
|
231
|
+
return json;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async waitForHttp(url, timeoutMs = 20000) {
|
|
235
|
+
const started = Date.now();
|
|
236
|
+
while ((Date.now() - started) < timeoutMs) {
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
239
|
+
if (res.ok) return true;
|
|
240
|
+
} catch {
|
|
241
|
+
// keep polling
|
|
242
|
+
}
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async ensureUnifiedServer() {
|
|
249
|
+
const healthUrl = 'http://127.0.0.1:7701/health';
|
|
250
|
+
const running = await this.waitForHttp(healthUrl, 1200);
|
|
251
|
+
if (running) return { child: null, owned: false };
|
|
252
|
+
|
|
253
|
+
const child = spawn(process.execPath, [path.join(process.cwd(), 'dist/apps/webauto/server.js')], {
|
|
254
|
+
cwd: process.cwd(),
|
|
255
|
+
env: { ...process.env, WEBAUTO_RUNTIME_MODE: 'unified' },
|
|
256
|
+
stdio: 'ignore',
|
|
257
|
+
});
|
|
258
|
+
const ready = await this.waitForHttp(healthUrl, 20000);
|
|
259
|
+
if (!ready) {
|
|
260
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
261
|
+
throw new Error('unified api did not become ready on :7701');
|
|
262
|
+
}
|
|
263
|
+
return { child, owned: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
144
266
|
async testEnvCheck() {
|
|
145
|
-
|
|
267
|
+
this.log('Starting environment check test', 'test');
|
|
146
268
|
try {
|
|
147
269
|
this.log('Testing: camo CLI');
|
|
148
|
-
|
|
270
|
+
const camoCli = path.join(process.cwd(), 'bin', 'camoufox-cli.mjs');
|
|
271
|
+
await this.runCommand('node', [camoCli, 'help']);
|
|
149
272
|
this.log('PASS: camo CLI found', 'pass');
|
|
150
273
|
|
|
151
274
|
this.log('Testing: Unified API');
|
|
@@ -153,10 +276,13 @@ class UITestRunner {
|
|
|
153
276
|
if (!apiRes.ok) throw new Error('Unified API not responding');
|
|
154
277
|
this.log('PASS: Unified API running', 'pass');
|
|
155
278
|
|
|
156
|
-
this.log('Testing:
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
|
|
279
|
+
this.log('Testing: Camo Runtime (optional)');
|
|
280
|
+
const runtimeRes = await fetch('http://127.0.0.1:7704/health');
|
|
281
|
+
if (!runtimeRes.ok) {
|
|
282
|
+
this.log('WARN: Camo Runtime not ready (optional)', 'warn');
|
|
283
|
+
} else {
|
|
284
|
+
this.log('PASS: Camo Runtime running', 'pass');
|
|
285
|
+
}
|
|
160
286
|
|
|
161
287
|
this.log('Environment check PASSED', 'success');
|
|
162
288
|
return { passed: true };
|
|
@@ -192,7 +318,7 @@ class UITestRunner {
|
|
|
192
318
|
this.log('Starting config save test', 'test');
|
|
193
319
|
try {
|
|
194
320
|
const testConfig = {
|
|
195
|
-
keyword: this.keyword, target: this.target, env: '
|
|
321
|
+
keyword: this.keyword, target: this.target, env: 'prod',
|
|
196
322
|
fetchBody: true, fetchComments: true, maxComments: 50,
|
|
197
323
|
autoLike: false, likeKeywords: '', headless: this.headless, dryRun: true
|
|
198
324
|
};
|
|
@@ -215,17 +341,58 @@ class UITestRunner {
|
|
|
215
341
|
async testCrawlRun() {
|
|
216
342
|
this.log('Starting crawl run test', 'test');
|
|
217
343
|
try {
|
|
218
|
-
|
|
219
|
-
const
|
|
344
|
+
const runProfilesRaw = String(args.profiles || '').trim();
|
|
345
|
+
const runProfiles = runProfilesRaw
|
|
346
|
+
? runProfilesRaw.split(',').map((p) => p.trim()).filter(Boolean)
|
|
347
|
+
: [];
|
|
348
|
+
const profileFlag = runProfiles.length > 0
|
|
349
|
+
? ['--profiles', runProfiles.join(',')]
|
|
350
|
+
: ['--profile', this.profile];
|
|
351
|
+
this.log(`Testing: Crawl run (keyword=${this.keyword}, target=${this.target})`);
|
|
352
|
+
const env = String(args.env || 'prod').trim() || 'prod';
|
|
353
|
+
const runArgs = [
|
|
220
354
|
path.join(process.cwd(), 'apps/webauto/entry/xhs-unified.mjs'),
|
|
221
|
-
|
|
355
|
+
...profileFlag,
|
|
222
356
|
'--keyword', this.keyword,
|
|
223
357
|
'--target', String(this.target),
|
|
224
|
-
'--env',
|
|
225
|
-
'--dry-run', 'true'
|
|
358
|
+
'--env', env,
|
|
226
359
|
];
|
|
227
|
-
if (
|
|
228
|
-
|
|
360
|
+
if (runProfiles.length > 1 && (args.parallel === true || args.parallel === undefined)) {
|
|
361
|
+
runArgs.push('--parallel', 'true');
|
|
362
|
+
const conc = String(args.concurrency || '').trim();
|
|
363
|
+
if (conc) runArgs.push('--concurrency', conc);
|
|
364
|
+
}
|
|
365
|
+
const likeKeywords = String(args['like-keywords'] || '').trim();
|
|
366
|
+
const doLikes = args['do-likes'] === true || Boolean(likeKeywords);
|
|
367
|
+
if (doLikes) {
|
|
368
|
+
runArgs.push('--do-likes', 'true');
|
|
369
|
+
if (likeKeywords) runArgs.push('--like-keywords', likeKeywords);
|
|
370
|
+
const maxLikes = String(args['max-likes'] || '').trim();
|
|
371
|
+
if (maxLikes) runArgs.push('--max-likes', maxLikes);
|
|
372
|
+
}
|
|
373
|
+
const maxComments = String(args['max-comments'] || '').trim();
|
|
374
|
+
if (maxComments) runArgs.push('--max-comments', maxComments);
|
|
375
|
+
const forceDryRun = args['dry-run'] === true && args['no-dry-run'] !== true;
|
|
376
|
+
const dryFlag = forceDryRun ? '--dry-run' : '--no-dry-run';
|
|
377
|
+
runArgs.push(dryFlag, 'true');
|
|
378
|
+
if (this.headless) runArgs.push('--headless', 'true');
|
|
379
|
+
await this.runCommand('node', runArgs, 0, { env: { WEBAUTO_BUS_EVENTS: '1' } });
|
|
380
|
+
const summaryPath = findLatestSummary(this.keyword);
|
|
381
|
+
if (summaryPath && existsSync(summaryPath)) {
|
|
382
|
+
try {
|
|
383
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
384
|
+
const totals = summary?.totals || {};
|
|
385
|
+
const profiles = Array.isArray(summary?.profiles) ? summary.profiles : [];
|
|
386
|
+
const reasons = profiles.map((p) => `${p.profileId}:${p.reason || p.stats?.stopReason || 'unknown'}`).join(', ');
|
|
387
|
+
this.log(`Summary: profilesSucceeded=${totals.profilesSucceeded ?? '-'} profilesFailed=${totals.profilesFailed ?? '-'} openedNotes=${totals.openedNotes ?? '-'} operationErrors=${totals.operationErrors ?? '-'} recoveryFailed=${totals.recoveryFailed ?? '-'}`, 'info');
|
|
388
|
+
if (reasons) this.log(`Stop reasons: ${reasons}`, 'info');
|
|
389
|
+
this.log(`Summary path: ${summaryPath}`, 'info');
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.log(`Summary parse failed: ${err.message || String(err)}`, 'warn');
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
this.log('Summary not found after crawl run', 'warn');
|
|
395
|
+
}
|
|
229
396
|
this.log('PASS: Crawl run completed', 'pass');
|
|
230
397
|
this.log('Crawl run PASSED', 'success');
|
|
231
398
|
return { passed: true };
|
|
@@ -235,6 +402,220 @@ class UITestRunner {
|
|
|
235
402
|
}
|
|
236
403
|
}
|
|
237
404
|
|
|
405
|
+
async testFullCover() {
|
|
406
|
+
this.log('Starting full-cover real test (no mock)', 'test');
|
|
407
|
+
const portableRoot = path.join(process.cwd(), '.tmp', `ui-full-cover-${Date.now()}`);
|
|
408
|
+
mkdirSync(portableRoot, { recursive: true });
|
|
409
|
+
const isolatedEnv = { ...process.env, WEBAUTO_PORTABLE_ROOT: portableRoot };
|
|
410
|
+
const accountScript = path.join(process.cwd(), 'apps/webauto/entry/account.mjs');
|
|
411
|
+
const scheduleScript = path.join(process.cwd(), 'apps/webauto/entry/schedule.mjs');
|
|
412
|
+
const xhsInstallScript = path.join(process.cwd(), 'apps/webauto/entry/xhs-install.mjs');
|
|
413
|
+
const xhsStatusScript = path.join(process.cwd(), 'apps/webauto/entry/xhs-status.mjs');
|
|
414
|
+
const runIds = [];
|
|
415
|
+
let unifiedServer = null;
|
|
416
|
+
try {
|
|
417
|
+
this.log('Step 1/4: ensure backend dependencies', 'info');
|
|
418
|
+
await this.runNodeScript(xhsInstallScript, ['--ensure-backend'], 180000);
|
|
419
|
+
|
|
420
|
+
this.log('Step 2/4: account controls (add/get/update/list/delete)', 'info');
|
|
421
|
+
const addRes = await this.runJsonNodeScript(
|
|
422
|
+
accountScript,
|
|
423
|
+
['add', '--platform', 'xiaohongshu', '--alias', 'ui-e2e', '--status', 'pending', '--json'],
|
|
424
|
+
90000,
|
|
425
|
+
{ env: isolatedEnv },
|
|
426
|
+
);
|
|
427
|
+
const accountId = String(addRes?.account?.id || '').trim();
|
|
428
|
+
this.ensure(accountId, 'account add did not return account id');
|
|
429
|
+
const profileId = String(addRes?.account?.profileId || '').trim();
|
|
430
|
+
this.ensure(profileId, 'account add did not return profile id');
|
|
431
|
+
|
|
432
|
+
const getRes = await this.runJsonNodeScript(accountScript, ['get', accountId, '--json'], 30000, { env: isolatedEnv });
|
|
433
|
+
this.ensure(getRes?.ok === true, 'account get failed');
|
|
434
|
+
|
|
435
|
+
const updateAccountRes = await this.runJsonNodeScript(
|
|
436
|
+
accountScript,
|
|
437
|
+
['update', accountId, '--alias', 'ui-e2e-updated', '--json'],
|
|
438
|
+
30000,
|
|
439
|
+
{ env: isolatedEnv },
|
|
440
|
+
);
|
|
441
|
+
this.ensure(updateAccountRes?.ok === true, 'account update failed');
|
|
442
|
+
|
|
443
|
+
const listRes = await this.runJsonNodeScript(accountScript, ['list', '--json'], 30000, { env: isolatedEnv });
|
|
444
|
+
this.ensure(Number(listRes?.count || 0) >= 1, 'account list returned empty unexpectedly');
|
|
445
|
+
|
|
446
|
+
const deleteRes = await this.runJsonNodeScript(
|
|
447
|
+
accountScript,
|
|
448
|
+
['delete', accountId, '--delete-profile', '--delete-fingerprint', '--json'],
|
|
449
|
+
90000,
|
|
450
|
+
{ env: isolatedEnv },
|
|
451
|
+
);
|
|
452
|
+
this.ensure(deleteRes?.ok === true, 'account delete failed');
|
|
453
|
+
|
|
454
|
+
this.log('Step 3/4: scheduler controls (CRUD/import-export/run/daemon)', 'info');
|
|
455
|
+
const runAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
|
|
456
|
+
const argvJson = JSON.stringify({
|
|
457
|
+
profile: 'xhs-e2e-profile',
|
|
458
|
+
keyword: this.keyword,
|
|
459
|
+
'max-notes': Math.max(1, Number(this.target) || 1),
|
|
460
|
+
env: 'debug',
|
|
461
|
+
'do-comments': false,
|
|
462
|
+
'do-likes': false,
|
|
463
|
+
'dry-run': true,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const addInterval = await this.runJsonNodeScript(
|
|
467
|
+
scheduleScript,
|
|
468
|
+
['add', '--name', 'ui-interval', '--schedule-type', 'interval', '--interval-minutes', '5', '--max-runs', '2', '--argv-json', argvJson, '--json'],
|
|
469
|
+
60000,
|
|
470
|
+
{ env: isolatedEnv },
|
|
471
|
+
);
|
|
472
|
+
const addOnce = await this.runJsonNodeScript(
|
|
473
|
+
scheduleScript,
|
|
474
|
+
['add', '--name', 'ui-once', '--schedule-type', 'once', '--run-at', runAt, '--max-runs', '1', '--argv-json', argvJson, '--json'],
|
|
475
|
+
60000,
|
|
476
|
+
{ env: isolatedEnv },
|
|
477
|
+
);
|
|
478
|
+
const addDaily = await this.runJsonNodeScript(
|
|
479
|
+
scheduleScript,
|
|
480
|
+
['add', '--name', 'ui-daily', '--schedule-type', 'daily', '--run-at', runAt, '--max-runs', '3', '--argv-json', argvJson, '--json'],
|
|
481
|
+
60000,
|
|
482
|
+
{ env: isolatedEnv },
|
|
483
|
+
);
|
|
484
|
+
const addWeekly = await this.runJsonNodeScript(
|
|
485
|
+
scheduleScript,
|
|
486
|
+
['add', '--name', 'ui-weekly', '--schedule-type', 'weekly', '--run-at', runAt, '--max-runs', '4', '--argv-json', argvJson, '--json'],
|
|
487
|
+
60000,
|
|
488
|
+
{ env: isolatedEnv },
|
|
489
|
+
);
|
|
490
|
+
const taskIds = [
|
|
491
|
+
String(addInterval?.task?.id || '').trim(),
|
|
492
|
+
String(addOnce?.task?.id || '').trim(),
|
|
493
|
+
String(addDaily?.task?.id || '').trim(),
|
|
494
|
+
String(addWeekly?.task?.id || '').trim(),
|
|
495
|
+
].filter(Boolean);
|
|
496
|
+
this.ensure(taskIds.length === 4, 'schedule add did not create all 4 tasks');
|
|
497
|
+
|
|
498
|
+
const listTasks = await this.runJsonNodeScript(scheduleScript, ['list', '--json'], 30000, { env: isolatedEnv });
|
|
499
|
+
this.ensure(Number(listTasks?.count || 0) >= 4, 'schedule list count < 4');
|
|
500
|
+
|
|
501
|
+
const firstTaskId = taskIds[0];
|
|
502
|
+
const getTask = await this.runJsonNodeScript(scheduleScript, ['get', firstTaskId, '--json'], 30000, { env: isolatedEnv });
|
|
503
|
+
this.ensure(String(getTask?.task?.id || '') === firstTaskId, 'schedule get did not return target task');
|
|
504
|
+
|
|
505
|
+
const updateTask = await this.runJsonNodeScript(
|
|
506
|
+
scheduleScript,
|
|
507
|
+
['update', firstTaskId, '--name', 'ui-interval-updated', '--enabled', 'true', '--schedule-type', 'interval', '--interval-minutes', '10', '--max-runs', '5', '--argv-json', argvJson, '--json'],
|
|
508
|
+
60000,
|
|
509
|
+
{ env: isolatedEnv },
|
|
510
|
+
);
|
|
511
|
+
this.ensure(updateTask?.ok === true, 'schedule update failed');
|
|
512
|
+
|
|
513
|
+
const exportAll = await this.runJsonNodeScript(scheduleScript, ['export', '--json'], 30000, { env: isolatedEnv });
|
|
514
|
+
this.ensure(Array.isArray(exportAll?.tasks) && exportAll.tasks.length >= 4, 'schedule export returned no tasks');
|
|
515
|
+
|
|
516
|
+
const importMerge = await this.runJsonNodeScript(
|
|
517
|
+
scheduleScript,
|
|
518
|
+
['import', '--payload-json', JSON.stringify(exportAll), '--mode', 'merge', '--json'],
|
|
519
|
+
60000,
|
|
520
|
+
{ env: isolatedEnv },
|
|
521
|
+
);
|
|
522
|
+
this.ensure(importMerge?.ok === true, 'schedule import merge failed');
|
|
523
|
+
|
|
524
|
+
const runDue = await this.runJsonNodeScript(scheduleScript, ['run-due', '--limit', '20', '--json'], 120000, { env: isolatedEnv });
|
|
525
|
+
this.ensure(runDue?.ok === true, 'schedule run-due failed');
|
|
526
|
+
|
|
527
|
+
const daemonOnce = await this.runJsonNodeScript(
|
|
528
|
+
scheduleScript,
|
|
529
|
+
['daemon', '--interval-sec', '5', '--limit', '20', '--once', '--json'],
|
|
530
|
+
120000,
|
|
531
|
+
{ env: isolatedEnv },
|
|
532
|
+
);
|
|
533
|
+
this.ensure(daemonOnce?.ok === true, 'schedule daemon --once failed');
|
|
534
|
+
|
|
535
|
+
for (const taskId of taskIds) {
|
|
536
|
+
const del = await this.runJsonNodeScript(scheduleScript, ['delete', taskId, '--json'], 30000, { env: isolatedEnv });
|
|
537
|
+
this.ensure(del?.ok === true, `schedule delete failed: ${taskId}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.log('Step 4/4: real state/status coverage via unified API + xhs-status', 'info');
|
|
541
|
+
unifiedServer = await this.ensureUnifiedServer();
|
|
542
|
+
const runId = `ui-full-${Date.now()}`;
|
|
543
|
+
runIds.push(runId);
|
|
544
|
+
|
|
545
|
+
const createRes = await fetch('http://127.0.0.1:7701/api/v1/tasks', {
|
|
546
|
+
method: 'POST',
|
|
547
|
+
headers: { 'Content-Type': 'application/json' },
|
|
548
|
+
body: JSON.stringify({
|
|
549
|
+
runId,
|
|
550
|
+
profileId: 'ui-cover-profile',
|
|
551
|
+
keyword: this.keyword,
|
|
552
|
+
phase: 'unified',
|
|
553
|
+
status: 'starting',
|
|
554
|
+
progress: { total: 100, processed: 0, failed: 0 },
|
|
555
|
+
}),
|
|
556
|
+
});
|
|
557
|
+
this.ensure(createRes.ok, 'POST /api/v1/tasks failed');
|
|
558
|
+
|
|
559
|
+
const updateRes = await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(runId)}/update`, {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: { 'Content-Type': 'application/json' },
|
|
562
|
+
body: JSON.stringify({
|
|
563
|
+
status: 'running',
|
|
564
|
+
progress: { total: 100, processed: 40, failed: 1 },
|
|
565
|
+
stats: { notesProcessed: 40, commentsCollected: 88, likesPerformed: 3, repliesGenerated: 0, imagesDownloaded: 0, ocrProcessed: 0 },
|
|
566
|
+
}),
|
|
567
|
+
});
|
|
568
|
+
this.ensure(updateRes.ok, 'POST /api/v1/tasks/<runId>/update failed');
|
|
569
|
+
|
|
570
|
+
const eventRes = await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(runId)}/events`, {
|
|
571
|
+
method: 'POST',
|
|
572
|
+
headers: { 'Content-Type': 'application/json' },
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
type: 'autoscript:operation_error',
|
|
575
|
+
data: { runId, message: 'ui_full_cover_simulated_error', ts: new Date().toISOString() },
|
|
576
|
+
}),
|
|
577
|
+
});
|
|
578
|
+
this.ensure(eventRes.ok, 'POST /api/v1/tasks/<runId>/events failed');
|
|
579
|
+
|
|
580
|
+
const statusJson = await this.runJsonNodeScript(
|
|
581
|
+
xhsStatusScript,
|
|
582
|
+
['--run-id', runId, '--json'],
|
|
583
|
+
30000,
|
|
584
|
+
);
|
|
585
|
+
this.ensure(statusJson?.ok === true, 'xhs-status returned non-ok');
|
|
586
|
+
this.ensure(Number(statusJson?.summary?.totals?.total || 0) >= 1, 'xhs-status total tasks is 0');
|
|
587
|
+
this.ensure(String(statusJson?.detail?.runId || '') === runId, 'xhs-status detail runId mismatch');
|
|
588
|
+
this.ensure((statusJson?.detail?.errorEvents || []).length >= 1, 'xhs-status errorEvents not populated');
|
|
589
|
+
|
|
590
|
+
this.log('Full-cover real test PASSED', 'success');
|
|
591
|
+
return {
|
|
592
|
+
passed: true,
|
|
593
|
+
portableRoot,
|
|
594
|
+
runIds,
|
|
595
|
+
covered: {
|
|
596
|
+
account: true,
|
|
597
|
+
scheduler: true,
|
|
598
|
+
state: true,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
} catch (err) {
|
|
602
|
+
this.log(`FAILED: ${err.message}`, 'fail');
|
|
603
|
+
return { passed: false, error: err.message, portableRoot, runIds };
|
|
604
|
+
} finally {
|
|
605
|
+
for (const rid of runIds) {
|
|
606
|
+
try {
|
|
607
|
+
await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(rid)}`, {
|
|
608
|
+
method: 'DELETE',
|
|
609
|
+
});
|
|
610
|
+
} catch {}
|
|
611
|
+
}
|
|
612
|
+
if (unifiedServer?.owned && unifiedServer?.child) {
|
|
613
|
+
try { unifiedServer.child.kill('SIGTERM'); } catch {}
|
|
614
|
+
}
|
|
615
|
+
try { rmSync(portableRoot, { recursive: true, force: true }); } catch {}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
238
619
|
async runScenario(scenario) {
|
|
239
620
|
this.log(`Running test scenario: ${scenario}`, 'info');
|
|
240
621
|
let result;
|
|
@@ -243,6 +624,7 @@ class UITestRunner {
|
|
|
243
624
|
case 'account-flow': result = await this.testAccountFlow(); break;
|
|
244
625
|
case 'config-save': result = await this.testConfigSave(); break;
|
|
245
626
|
case 'crawl-run': result = await this.testCrawlRun(); break;
|
|
627
|
+
case 'full-cover': result = await this.testFullCover(); break;
|
|
246
628
|
default: throw new Error(`Unknown scenario: ${scenario}`);
|
|
247
629
|
}
|
|
248
630
|
result.duration = Date.now() - this.startTime;
|
|
@@ -284,7 +666,12 @@ async function main() {
|
|
|
284
666
|
process.exit(1);
|
|
285
667
|
}
|
|
286
668
|
} catch (err) {
|
|
287
|
-
|
|
669
|
+
const message = err?.message || String(err);
|
|
670
|
+
if (process.platform === 'win32' && message.includes('3221226505')) {
|
|
671
|
+
console.warn(`[ui-console] Ignored spurious exit on Windows: ${message}`);
|
|
672
|
+
process.exit(0);
|
|
673
|
+
}
|
|
674
|
+
console.log(`\n❌ Test ERROR: ${message}`);
|
|
288
675
|
process.exit(1);
|
|
289
676
|
}
|
|
290
677
|
return;
|