This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { ToolDef } from '../../llm/openai-compat.js';
|
||||
import type { ToolContext, ToolResult } from './core.js';
|
||||
import type { ToolContext, ToolResult } from './tool-context.js';
|
||||
import { resolveAndGuard } from './core.js';
|
||||
import { logger } from '../../logger.js';
|
||||
import { loadConfig } from '../../config.js';
|
||||
@@ -49,7 +49,6 @@ export interface ExpectItem {
|
||||
}
|
||||
|
||||
interface ValidatedInput {
|
||||
space: string;
|
||||
app: string;
|
||||
entry: string;
|
||||
steps: Record<string, unknown>[];
|
||||
@@ -67,11 +66,6 @@ export function validateInput(raw: Record<string, unknown>): ValidatedInput {
|
||||
if (typeof app !== 'string' || !app.trim()) {
|
||||
throw new Error('TestWorkspaceApp: "app" (string) is required');
|
||||
}
|
||||
const space = raw['space'];
|
||||
if (typeof space !== 'string' || !space.trim()) {
|
||||
throw new Error('TestWorkspaceApp: "space" (string) is required');
|
||||
}
|
||||
|
||||
const entry =
|
||||
typeof raw['entry'] === 'string' && raw['entry'].trim()
|
||||
? raw['entry'].trim()
|
||||
@@ -105,12 +99,12 @@ export function validateInput(raw: Record<string, unknown>): ValidatedInput {
|
||||
}
|
||||
}
|
||||
|
||||
const timeout_ms =
|
||||
const timeout_ms = Math.min(120_000,
|
||||
typeof raw['timeout_ms'] === 'number' && raw['timeout_ms'] > 0
|
||||
? raw['timeout_ms']
|
||||
: 30_000;
|
||||
: 30_000);
|
||||
|
||||
return { space: space.trim(), app: app.trim(), entry, steps, seed_files, expect: expectRaw, timeout_ms };
|
||||
return { app: app.trim(), entry, steps, seed_files, expect: expectRaw, timeout_ms };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -122,6 +116,7 @@ export interface EvaluateExpectationsContext {
|
||||
pageText: string;
|
||||
/** Absolute path to the workspace root */
|
||||
workspacePath: string;
|
||||
targetedText?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ExpectationFailure {
|
||||
@@ -147,7 +142,10 @@ export function evaluateExpectations(
|
||||
if (exp.kind === 'text') {
|
||||
// target is treated as a hint/CSS selector (informational), contains is checked against page text
|
||||
const needle = exp.contains ?? '';
|
||||
if (!ctx.pageText.includes(needle)) {
|
||||
const haystack = exp.target && exp.target !== 'body'
|
||||
? (ctx.targetedText?.[exp.target] ?? '')
|
||||
: ctx.pageText;
|
||||
if (!haystack.includes(needle)) {
|
||||
failures.push(
|
||||
`text[${i}]: target="${exp.target ?? ''}" — "${needle}" not found in page text`,
|
||||
);
|
||||
@@ -195,10 +193,6 @@ export const TOOL_DEFS: Record<string, ToolDef> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'string',
|
||||
description: 'テスト対象スペースの ID',
|
||||
},
|
||||
app: {
|
||||
type: 'string',
|
||||
description: 'apps/ 配下のアプリフォルダ名(パスではなく名前のみ)',
|
||||
@@ -268,7 +262,7 @@ export const TOOL_DEFS: Record<string, ToolDef> = {
|
||||
description: 'ブラウザ操作全体のタイムアウト(ミリ秒、デフォルト 30000)',
|
||||
},
|
||||
},
|
||||
required: ['space', 'app'],
|
||||
required: ['app'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -316,7 +310,7 @@ export async function executeTool(
|
||||
return { output: (err as Error).message, isError: true };
|
||||
}
|
||||
|
||||
const { space, app, entry, steps, seed_files, expect: expectations, timeout_ms } = validated;
|
||||
const { app, entry, steps, seed_files, expect: expectations, timeout_ms } = validated;
|
||||
|
||||
const result: AppTestResult = {
|
||||
ok: false,
|
||||
@@ -348,7 +342,7 @@ export async function executeTool(
|
||||
|
||||
try {
|
||||
// Build harness URL
|
||||
const harnessUrl = buildHarnessUrl(port, { space, app, entry, token });
|
||||
const harnessUrl = buildHarnessUrl(port, { space: spaceId, app, entry, token });
|
||||
|
||||
// Dynamically import browser module (avoids Playwright load when unused)
|
||||
const { runPageActions } = await import('./browser.js');
|
||||
@@ -366,6 +360,8 @@ export async function executeTool(
|
||||
{ type: 'getText' },
|
||||
// User steps
|
||||
...(steps as unknown as BrowseWebAction[]),
|
||||
...expectations.filter(item => item.kind === 'text' && item.target && item.target !== 'body')
|
||||
.map(item => ({ type: 'getText' as const, selector: item.target })),
|
||||
// Final screenshot
|
||||
{ type: 'screenshot', value: `app-test-${app}-final.png` },
|
||||
];
|
||||
@@ -377,19 +373,38 @@ export async function executeTool(
|
||||
if (a.type === 'screenshot' && a.segments === undefined) a.segments = false;
|
||||
}
|
||||
|
||||
const pageResult = await runPageActions(harnessUrl, builtActions, ctx, {
|
||||
isolatedContext: true,
|
||||
extraAllowedHosts: ['localhost', '127.0.0.1'],
|
||||
});
|
||||
const timeoutController = new AbortController();
|
||||
const timeoutHandle = setTimeout(() => timeoutController.abort(), timeout_ms);
|
||||
let pageResult: Awaited<ReturnType<typeof runPageActions>>;
|
||||
try {
|
||||
pageResult = await runPageActions(harnessUrl, builtActions, ctx, {
|
||||
isolatedContext: true,
|
||||
extraAllowedHosts: ['localhost', '127.0.0.1'],
|
||||
defaultToFirstChildFrame: true,
|
||||
abortSignal: timeoutController.signal,
|
||||
});
|
||||
if (timeoutController.signal.aborted) throw new Error(`timed out after ${timeout_ms}ms`);
|
||||
} catch (error) {
|
||||
if (timeoutController.signal.aborted) throw new Error(`timed out after ${timeout_ms}ms`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
|
||||
result.screenshots = pageResult.screenshots;
|
||||
result.console_errors = pageResult.consoleErrors;
|
||||
for (const error of result.console_errors) result.failures.push(`app diagnostic: ${error}`);
|
||||
|
||||
// Evaluate expectations using the extracted pure function
|
||||
const pageText = pageResult.texts.join('\n');
|
||||
const targetedText: Record<string, string> = {};
|
||||
const targeted = expectations.filter(item => item.kind === 'text' && item.target && item.target !== 'body');
|
||||
const captured = pageResult.texts.slice(-targeted.length);
|
||||
targeted.forEach((item, index) => { targetedText[item.target] = captured[index] ?? ''; });
|
||||
const expectFailures = evaluateExpectations(expectations, {
|
||||
pageText,
|
||||
workspacePath: ctx.workspacePath,
|
||||
targetedText,
|
||||
});
|
||||
result.failures.push(...expectFailures);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user