mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: release v1.40.0
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const DEFAULTS = {
|
||||
appUrl: "http://127.0.0.1:1420/",
|
||||
healthUrl: "http://127.0.0.1:3030/health",
|
||||
timeoutMs: 120_000,
|
||||
intervalMs: 1_000,
|
||||
evidenceDir: path.join(process.cwd(), ".lime", "qc", "gui-evidence", "agent-app-lab"),
|
||||
prefix: "agent-app-lab-smoke",
|
||||
};
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--app-url" && argv[index + 1]) {
|
||||
options.appUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--health-url" && argv[index + 1]) {
|
||||
options.healthUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--timeout-ms" && argv[index + 1]) {
|
||||
options.timeoutMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--interval-ms" && argv[index + 1]) {
|
||||
options.intervalMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--evidence-dir" && argv[index + 1]) {
|
||||
options.evidenceDir = path.resolve(String(argv[index + 1]).trim());
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--prefix" && argv[index + 1]) {
|
||||
options.prefix = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function logStage(stage) {
|
||||
console.log(`[smoke:agent-app-lab] stage=${stage}`);
|
||||
}
|
||||
|
||||
async function launchSmokeContext(userDataDir) {
|
||||
try {
|
||||
return await chromium.launchPersistentContext(userDataDir, {
|
||||
channel: "chrome",
|
||||
headless: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[smoke:agent-app-lab] Chrome channel 启动失败,尝试 Playwright Chromium: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
return chromium.launchPersistentContext(userDataDir, {
|
||||
headless: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForHealth(options) {
|
||||
const startedAt = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - startedAt < options.timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(options.healthUrl);
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
console.log(
|
||||
`[smoke:agent-app-lab] DevBridge 已就绪 (${Date.now() - startedAt}ms) status=${payload?.status ?? "unknown"}`,
|
||||
);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await sleep(options.intervalMs);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`[smoke:agent-app-lab] DevBridge 未就绪: ${
|
||||
lastError instanceof Error ? lastError.message : String(lastError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function runFlagOffRegression(options) {
|
||||
logStage("flag-off-regression");
|
||||
const userDataDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "lime-agent-app-lab-flag-off-"),
|
||||
);
|
||||
const context = await launchSmokeContext(userDataDir);
|
||||
const consoleErrors = [];
|
||||
const failedRequests = [];
|
||||
|
||||
await context.addInitScript(() => {
|
||||
window.localStorage.removeItem("lime.agentAppHost.flags");
|
||||
window.localStorage.removeItem("lime.agentAppHost.labEnabled");
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
failedRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
failure: request.failure()?.errorText ?? "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(options.appUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="app-sidebar-main-nav"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
const assertions = {
|
||||
labNavHidden: (await page.locator('button[title="Agent App Lab"]').count()) === 0,
|
||||
labPageHidden: (await page.locator('[data-testid="agent-app-lab-page"]').count()) === 0,
|
||||
agentAppsNavVisible: (await page.locator('button[title="Agent Apps"]').count()) > 0,
|
||||
noConsoleErrors: consoleErrors.length === 0,
|
||||
};
|
||||
|
||||
Object.entries(assertions).forEach(([key, value]) => {
|
||||
if (!value && key === "noConsoleErrors") {
|
||||
console.error(
|
||||
`[smoke:agent-app-lab] flagOffConsoleErrors=${JSON.stringify(consoleErrors, null, 2)}`,
|
||||
);
|
||||
}
|
||||
assert(Boolean(value), `Flag-off assertion failed: ${key}`);
|
||||
});
|
||||
|
||||
const screenshotPath = path.join(options.evidenceDir, `${options.prefix}-flag-off.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return {
|
||||
assertions,
|
||||
consoleErrors,
|
||||
failedRequests,
|
||||
screenshot: screenshotPath,
|
||||
};
|
||||
} finally {
|
||||
await context.close();
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
fs.mkdirSync(options.evidenceDir, { recursive: true });
|
||||
await waitForHealth(options);
|
||||
|
||||
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "lime-agent-app-lab-smoke-"));
|
||||
let context;
|
||||
context = await launchSmokeContext(userDataDir);
|
||||
const consoleErrors = [];
|
||||
const failedRequests = [];
|
||||
|
||||
await context.addInitScript(() => {
|
||||
window.localStorage.setItem(
|
||||
"lime.agentAppHost.flags",
|
||||
JSON.stringify({
|
||||
labEnabled: true,
|
||||
realAdapterEnabled: true,
|
||||
uiRuntimeEnabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
failedRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
failure: request.failure()?.errorText ?? "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
logStage("open-app");
|
||||
await page.goto(options.appUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
|
||||
await page.waitForSelector('button[title="Agent App Lab"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("open-agent-app-lab");
|
||||
await page.click('button[title="Agent App Lab"]');
|
||||
await page.waitForSelector('[data-testid="agent-app-lab-page"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-install-flow"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-manager"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-manager-repository"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-manager-repository-list"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector(
|
||||
'[data-testid="agent-app-manager-repository-app-content-factory-playbook-app"]',
|
||||
{
|
||||
timeout: options.timeoutMs,
|
||||
},
|
||||
);
|
||||
consoleErrors.length = 0;
|
||||
failedRequests.length = 0;
|
||||
|
||||
logStage("resolve-lab-setup");
|
||||
await page.click('[data-testid="agent-app-lab-resolve-setup"]');
|
||||
await page.waitForFunction(
|
||||
() => document.body.textContent?.includes("Guard 已允许") ||
|
||||
document.body.textContent?.includes("Guard allows launch") ||
|
||||
document.body.textContent?.includes("agentApp.lab.installFlow.launchReady"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
|
||||
logStage("select-companion-app");
|
||||
await page.click(
|
||||
'[data-testid="agent-app-manager-repository-app-content-factory-playbook-app"]',
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.querySelector('[data-testid="agent-app-manager-selected-app"]')
|
||||
?.textContent?.includes("fixture:content-factory-playbook-app"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
|
||||
logStage("verify-disable-blocker");
|
||||
await page.click('[data-testid="agent-app-manager-disable"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.querySelector('[data-testid="agent-app-manager-launch-entry-dashboard"]')
|
||||
?.hasAttribute("disabled") &&
|
||||
!document
|
||||
.querySelector('[data-testid="agent-app-manager-enable"]')
|
||||
?.hasAttribute("disabled"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
const managerDisableBlocked = await page.isDisabled(
|
||||
'[data-testid="agent-app-manager-launch-entry-dashboard"]',
|
||||
);
|
||||
const managerEnableAvailable = !(await page.isDisabled(
|
||||
'[data-testid="agent-app-manager-enable"]',
|
||||
));
|
||||
await page.click('[data-testid="agent-app-manager-enable"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!document
|
||||
.querySelector('[data-testid="agent-app-manager-launch-entry-dashboard"]')
|
||||
?.hasAttribute("disabled"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
const managerReenabled = !(await page.isDisabled(
|
||||
'[data-testid="agent-app-manager-launch-entry-dashboard"]',
|
||||
));
|
||||
|
||||
logStage("mount-dashboard");
|
||||
await page.click('[data-testid="agent-app-manager-launch-entry-dashboard"]');
|
||||
await page.waitForSelector('[data-testid="agent-app-ui-runtime-result"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-entry-runtime-guard-allow"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-manager-cleanup-evidence"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("preview-delete-data-cleanup-evidence");
|
||||
await page.click('[data-testid="agent-app-manager-uninstall-delete-data"]');
|
||||
await page.waitForSelector('[data-testid="agent-app-manager-evidence-json"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-manager-residual-audit"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const cleanupEvidenceText = await page.textContent(
|
||||
'[data-testid="agent-app-manager-evidence-json"]',
|
||||
);
|
||||
const cleanupEvidence = JSON.parse(cleanupEvidenceText ?? "{}");
|
||||
const residualPendingText = await page.textContent(
|
||||
'[data-testid="agent-app-manager-residual-pending"]',
|
||||
);
|
||||
|
||||
const pageText = await page.textContent("body");
|
||||
const uiRuntimeText = await page.textContent('[data-testid="agent-app-ui-runtime-result"]');
|
||||
const assertions = {
|
||||
labVisible: Boolean(await page.$('[data-testid="agent-app-lab-page"]')),
|
||||
installFlowVisible: Boolean(await page.$('[data-testid="agent-app-install-flow"]')),
|
||||
managerVisible: Boolean(await page.$('[data-testid="agent-app-manager"]')),
|
||||
managerRepository: Boolean(await page.$('[data-testid="agent-app-manager-repository"]')),
|
||||
managerRepositoryList: Boolean(
|
||||
await page.$('[data-testid="agent-app-manager-repository-list"]'),
|
||||
),
|
||||
managerMultiApp: Boolean(
|
||||
await page.$('[data-testid="agent-app-manager-repository-app-content-factory-playbook-app"]'),
|
||||
),
|
||||
managerSelectedApp: Boolean(
|
||||
pageText?.includes("fixture:content-factory-playbook-app"),
|
||||
),
|
||||
managerDisableBlocked,
|
||||
managerEnableAvailable,
|
||||
managerReenabled,
|
||||
selectedRuntimeApp: Boolean(
|
||||
uiRuntimeText?.includes("content-factory-playbook-app"),
|
||||
),
|
||||
setupResolved: pageText?.includes("Lab 示例 setup 已解决") ||
|
||||
pageText?.includes("Lab sample setup") ||
|
||||
pageText?.includes("agentApp.lab.installFlow.setupResolved"),
|
||||
guardAllowed: Boolean(await page.$('[data-testid="agent-app-entry-runtime-guard-allow"]')),
|
||||
uiMounted: Boolean(await page.$('[data-testid="agent-app-ui-runtime-result"]')),
|
||||
managerEvidence: Boolean(await page.$('[data-testid="agent-app-manager-cleanup-evidence"]')),
|
||||
cleanupEvidenceJson: Boolean(
|
||||
await page.$('[data-testid="agent-app-manager-evidence-json"]'),
|
||||
),
|
||||
residualAuditVisible: Boolean(
|
||||
await page.$('[data-testid="agent-app-manager-residual-audit"]'),
|
||||
),
|
||||
residualAuditPending: Boolean(
|
||||
residualPendingText?.includes("10") ||
|
||||
residualPendingText?.includes(
|
||||
"agentApp.lab.manager.evidence.residual.pendingDeletion",
|
||||
),
|
||||
),
|
||||
cleanupEvidenceSelectedApp: cleanupEvidence.appId === "content-factory-playbook-app",
|
||||
cleanupEvidenceStrategy: cleanupEvidence.strategy === "delete-data",
|
||||
cleanupEvidenceBlockedCount: cleanupEvidence.blockedTargetCount === 0,
|
||||
launchedStatus: pageText?.includes("已在 Lab 启动") ||
|
||||
pageText?.includes("Launched in Lab") ||
|
||||
pageText?.includes("agentApp.lab.installFlow.status.launched"),
|
||||
cleanupPreview: Boolean(
|
||||
await page.$('[data-testid="agent-app-install-flow-stage-cleanup-preview"]'),
|
||||
),
|
||||
noConsoleErrors: consoleErrors.length === 0,
|
||||
};
|
||||
|
||||
Object.entries(assertions).forEach(([key, value]) => {
|
||||
if (!value && key === "noConsoleErrors") {
|
||||
console.error(
|
||||
`[smoke:agent-app-lab] consoleErrors=${JSON.stringify(consoleErrors, null, 2)}`,
|
||||
);
|
||||
}
|
||||
assert(Boolean(value), `Assertion failed: ${key}`);
|
||||
});
|
||||
|
||||
const flagOff = await runFlagOffRegression(options);
|
||||
const flagOffAssertions = {
|
||||
flagOffLabNavHidden: flagOff.assertions.labNavHidden,
|
||||
flagOffLabPageHidden: flagOff.assertions.labPageHidden,
|
||||
flagOffAgentAppsNavVisible: flagOff.assertions.agentAppsNavVisible,
|
||||
flagOffNoConsoleErrors: flagOff.assertions.noConsoleErrors,
|
||||
};
|
||||
Object.entries(flagOffAssertions).forEach(([key, value]) => {
|
||||
assert(Boolean(value), `Assertion failed: ${key}`);
|
||||
});
|
||||
|
||||
const screenshotPath = path.join(options.evidenceDir, `${options.prefix}.png`);
|
||||
const summaryPath = path.join(options.evidenceDir, `${options.prefix}-summary.json`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
fs.writeFileSync(
|
||||
summaryPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
scenarioId: "agent-app-lab-smoke",
|
||||
appUrl: options.appUrl,
|
||||
assertions: {
|
||||
...assertions,
|
||||
...flagOffAssertions,
|
||||
},
|
||||
cleanupEvidence,
|
||||
residualAudit: {
|
||||
pendingText: residualPendingText,
|
||||
},
|
||||
flagOff,
|
||||
consoleErrors,
|
||||
failedRequests,
|
||||
screenshot: screenshotPath,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
console.log(`[smoke:agent-app-lab] summary=${summaryPath}`);
|
||||
console.log("[smoke:agent-app-lab] 通过");
|
||||
} finally {
|
||||
await context.close();
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const DEFAULTS = {
|
||||
appUrl: "http://127.0.0.1:1420/",
|
||||
healthUrl: "http://127.0.0.1:3030/health",
|
||||
timeoutMs: 120_000,
|
||||
intervalMs: 1_000,
|
||||
evidenceDir: path.join(process.cwd(), ".lime", "qc", "gui-evidence", "agent-apps"),
|
||||
prefix: "agent-apps-smoke",
|
||||
};
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--app-url" && argv[index + 1]) {
|
||||
options.appUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--health-url" && argv[index + 1]) {
|
||||
options.healthUrl = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--timeout-ms" && argv[index + 1]) {
|
||||
options.timeoutMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--interval-ms" && argv[index + 1]) {
|
||||
options.intervalMs = Number(argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--evidence-dir" && argv[index + 1]) {
|
||||
options.evidenceDir = path.resolve(String(argv[index + 1]).trim());
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--prefix" && argv[index + 1]) {
|
||||
options.prefix = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function logStage(stage) {
|
||||
console.log(`[smoke:agent-apps] stage=${stage}`);
|
||||
}
|
||||
|
||||
async function launchSmokeContext(userDataDir) {
|
||||
try {
|
||||
return await chromium.launchPersistentContext(userDataDir, {
|
||||
channel: "chrome",
|
||||
headless: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[smoke:agent-apps] Chrome channel 启动失败,尝试 Playwright Chromium: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
return chromium.launchPersistentContext(userDataDir, {
|
||||
headless: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForHealth(options) {
|
||||
const startedAt = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - startedAt < options.timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(options.healthUrl);
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
console.log(
|
||||
`[smoke:agent-apps] DevBridge 已就绪 (${Date.now() - startedAt}ms) status=${payload?.status ?? "unknown"}`,
|
||||
);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await sleep(options.intervalMs);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`[smoke:agent-apps] DevBridge 未就绪: ${
|
||||
lastError instanceof Error ? lastError.message : String(lastError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
function activeCloudBootstrapPayload() {
|
||||
return {
|
||||
schemaVersion: "agent-app-cloud-bootstrap/v1",
|
||||
tenantId: "smoke-formal-entry",
|
||||
generatedAt: "2026-05-16T00:00:00.000Z",
|
||||
apps: [
|
||||
{
|
||||
appId: "content-factory-app",
|
||||
displayName: "内容工厂",
|
||||
version: "0.3.0",
|
||||
releaseId: "smoke-content-factory-app-0.3.0",
|
||||
channel: "smoke",
|
||||
licenseState: "active",
|
||||
registrationRequired: true,
|
||||
registrationState: "active",
|
||||
enabled: true,
|
||||
packageUrl: "https://lime.local/agent-apps/content-factory-app/releases/0.3.0/package.zip",
|
||||
packageHash: "package-fnv1a-smoke-formal",
|
||||
manifestHash: "manifest-fnv1a-smoke-formal",
|
||||
capabilityRequirements: {},
|
||||
defaultEntries: ["dashboard", "content_scenario_planning"],
|
||||
policyDefaults: {},
|
||||
toolAvailability: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function runFlagOffRegression(options) {
|
||||
logStage("flag-off-regression");
|
||||
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "lime-agent-apps-flag-off-"));
|
||||
const context = await launchSmokeContext(userDataDir);
|
||||
const page = await context.newPage();
|
||||
const consoleErrors = [];
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(options.appUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="app-sidebar-main-nav"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
const assertions = {
|
||||
agentAppsNavVisible: (await page.locator('button[title="Agent Apps"]').count()) > 0,
|
||||
labNavHidden: (await page.locator('button[title="Agent App Lab"]').count()) === 0,
|
||||
noConsoleErrors: consoleErrors.length === 0,
|
||||
};
|
||||
|
||||
Object.entries(assertions).forEach(([key, value]) => {
|
||||
assert(Boolean(value), `Flag-off assertion failed: ${key}`);
|
||||
});
|
||||
|
||||
const screenshotPath = path.join(options.evidenceDir, `${options.prefix}-flag-off.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return {
|
||||
assertions,
|
||||
consoleErrors,
|
||||
screenshot: screenshotPath,
|
||||
};
|
||||
} finally {
|
||||
await context.close();
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
fs.mkdirSync(options.evidenceDir, { recursive: true });
|
||||
await waitForHealth(options);
|
||||
|
||||
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "lime-agent-apps-smoke-"));
|
||||
const context = await launchSmokeContext(userDataDir);
|
||||
const consoleErrors = [];
|
||||
const failedRequests = [];
|
||||
|
||||
const bootstrap = activeCloudBootstrapPayload();
|
||||
await context.addInitScript((payload) => {
|
||||
window.localStorage.removeItem("lime.agentAppHost.flags");
|
||||
window.localStorage.removeItem("lime.agentAppHost.labEnabled");
|
||||
window.__LIME_AGENT_APPS_SMOKE_BOOTSTRAP__ = payload;
|
||||
}, bootstrap);
|
||||
|
||||
const page = await context.newPage();
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
failedRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
failure: request.failure()?.errorText ?? "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
logStage("open-app");
|
||||
await page.goto(options.appUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
|
||||
await page.waitForSelector('[data-testid="app-sidebar-main-nav"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
assert(
|
||||
(await page.locator('button[title="Agent Apps"]').count()) > 0,
|
||||
"Agent Apps nav should be visible",
|
||||
);
|
||||
assert(
|
||||
(await page.locator('button[title="Agent App Lab"]').count()) === 0,
|
||||
"Agent App Lab nav should stay hidden in formal smoke",
|
||||
);
|
||||
|
||||
logStage("open-agent-apps");
|
||||
await page.click('button[title="Agent Apps"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-page"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-apps-installed-content-factory-app"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("verify-registration-required");
|
||||
await page.waitForSelector('[data-testid="agent-apps-registration-content-factory-app"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const registrationInstallBlocked = await page.isDisabled(
|
||||
'[data-testid="agent-apps-install-cloud-content-factory-app"]',
|
||||
);
|
||||
|
||||
logStage("activate-bootstrap-catalog");
|
||||
await page.evaluate((payload) => {
|
||||
window.__LIME_BOOTSTRAP__ = payload;
|
||||
}, bootstrap);
|
||||
await page.click('[data-testid="agent-apps-refresh"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!document
|
||||
.querySelector('[data-testid="agent-apps-install-cloud-content-factory-app"]')
|
||||
?.hasAttribute("disabled"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
|
||||
logStage("install-cloud-review");
|
||||
await page.click('[data-testid="agent-apps-install-cloud-content-factory-app"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-install-review"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.click('[data-testid="agent-apps-install-review-confirm"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-installed-content-factory-app"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("disable-enable");
|
||||
await page.click('[data-testid="agent-apps-installed-content-factory-app"]');
|
||||
await page.click('[data-testid="agent-apps-disable"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.querySelector('[data-testid="agent-apps-launch-entry-dashboard"]')
|
||||
?.hasAttribute("disabled") &&
|
||||
!document.querySelector('[data-testid="agent-apps-enable"]')?.hasAttribute("disabled"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
const disabledLaunchBlocked = await page.isDisabled(
|
||||
'[data-testid="agent-apps-launch-entry-dashboard"]',
|
||||
);
|
||||
await page.click('[data-testid="agent-apps-enable"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!document
|
||||
.querySelector('[data-testid="agent-apps-launch-entry-dashboard"]')
|
||||
?.hasAttribute("disabled"),
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
|
||||
logStage("launch-runtime-surface");
|
||||
await page.click('[data-testid="agent-apps-launch-entry-dashboard"]');
|
||||
await page.waitForSelector('[data-testid="agent-app-runtime-surface"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-app-runtime-frame"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const runtimeFrameSrc = await page.getAttribute(
|
||||
'[data-testid="agent-app-runtime-frame"]',
|
||||
"src",
|
||||
);
|
||||
|
||||
logStage("return-agent-apps");
|
||||
await page.click('button[title="Agent Apps"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-page"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-apps-installed-content-factory-app"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("uninstall-rehearsal");
|
||||
await page.click('[data-testid="agent-apps-uninstall-delete-data"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-uninstall-preview"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-apps-cleanup-evidence-json"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-apps-residual-audit"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const cleanupEvidenceText = await page.textContent(
|
||||
'[data-testid="agent-apps-cleanup-evidence-json"]',
|
||||
);
|
||||
const cleanupEvidence = JSON.parse(cleanupEvidenceText ?? "{}");
|
||||
await page.click('[data-testid="agent-apps-uninstall-confirm"]');
|
||||
await page.waitForSelector('[data-testid="agent-apps-launch-summary"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const stillInstalledAfterRehearsal =
|
||||
(await page.locator('[data-testid="agent-apps-installed-content-factory-app"]').count()) > 0;
|
||||
|
||||
const flagOff = await runFlagOffRegression(options);
|
||||
const assertions = {
|
||||
formalPageVisible: Boolean(await page.$('[data-testid="agent-apps-page"]')),
|
||||
installedVisible: stillInstalledAfterRehearsal,
|
||||
registrationRequiredBlocked: registrationInstallBlocked,
|
||||
cloudInstallReviewVisible: true,
|
||||
disabledLaunchBlocked,
|
||||
runtimeSurfaceVisible: Boolean(runtimeFrameSrc),
|
||||
cleanupEvidenceSelectedApp: cleanupEvidence.appId === "content-factory-app",
|
||||
cleanupEvidenceStrategy: cleanupEvidence.strategy === "delete-data",
|
||||
cleanupEvidenceRehearsalOnly: cleanupEvidence.completionEffect === "rehearsal-only",
|
||||
cleanupEvidenceBlockedCount: cleanupEvidence.blockedTargetCount === 0,
|
||||
flagOffAgentAppsNavVisible: flagOff.assertions.agentAppsNavVisible,
|
||||
flagOffLabNavHidden: flagOff.assertions.labNavHidden,
|
||||
flagOffNoConsoleErrors: flagOff.assertions.noConsoleErrors,
|
||||
};
|
||||
|
||||
Object.entries(assertions).forEach(([key, value]) => {
|
||||
assert(Boolean(value), `Assertion failed: ${key}`);
|
||||
});
|
||||
|
||||
const screenshotPath = path.join(options.evidenceDir, `${options.prefix}.png`);
|
||||
const summaryPath = path.join(options.evidenceDir, `${options.prefix}-summary.json`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
fs.writeFileSync(
|
||||
summaryPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
scenarioId: "agent-apps-smoke",
|
||||
appUrl: options.appUrl,
|
||||
assertions,
|
||||
runtimeFrameSrc,
|
||||
cleanupEvidence,
|
||||
flagOff,
|
||||
consoleErrors,
|
||||
failedRequests,
|
||||
screenshot: screenshotPath,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
console.log(`[smoke:agent-apps] summary=${summaryPath}`);
|
||||
console.log("[smoke:agent-apps] 通过");
|
||||
} finally {
|
||||
await context.close();
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
+123
-17
@@ -459,6 +459,98 @@ async function clickPageControl(page, { text, ariaLabel, index = 0 }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForInputbarKnowledgePackToggle(page, label, timeoutMs) {
|
||||
const locator = page.locator('[data-testid="inputbar-knowledge-pack-toggle"]');
|
||||
|
||||
try {
|
||||
await locator.first().waitFor({ state: "visible", timeout: timeoutMs });
|
||||
} catch (error) {
|
||||
const pageText = await page
|
||||
.locator("body")
|
||||
.innerText()
|
||||
.catch(() => "");
|
||||
const knowledgeControls = await page
|
||||
.locator("[data-testid^='inputbar-knowledge-']")
|
||||
.evaluateAll((items) =>
|
||||
items.slice(0, 40).map((item) => ({
|
||||
testId: item.getAttribute("data-testid"),
|
||||
text: (item.textContent || "").trim().replace(/\s+/g, " "),
|
||||
aria: item.getAttribute("aria-label"),
|
||||
title: item.getAttribute("title"),
|
||||
})),
|
||||
)
|
||||
.catch(() => []);
|
||||
throw new Error(
|
||||
`[smoke:knowledge-gui] ${label} 等待默认项目资料控件失败 ${JSON.stringify({
|
||||
knowledgeControls,
|
||||
})},页面文本预览: ${pageText.slice(0, 2_000)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function clickTestId(page, { testId, label }) {
|
||||
const locator = page.locator(`[data-testid="${testId}"]`).first();
|
||||
|
||||
try {
|
||||
await locator.click({ timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
||||
} catch (error) {
|
||||
const pageText = await page
|
||||
.locator("body")
|
||||
.innerText()
|
||||
.catch(() => "");
|
||||
const candidates = await page
|
||||
.locator("[data-testid]")
|
||||
.evaluateAll((items) =>
|
||||
items.slice(0, 100).map((item) => ({
|
||||
testId: item.getAttribute("data-testid"),
|
||||
text: (item.textContent || "").trim().replace(/\s+/g, " "),
|
||||
aria: item.getAttribute("aria-label"),
|
||||
title: item.getAttribute("title"),
|
||||
})),
|
||||
)
|
||||
.catch(() => []);
|
||||
throw new Error(
|
||||
`[smoke:knowledge-gui] ${label} 点击失败 ${JSON.stringify({
|
||||
testId,
|
||||
candidates,
|
||||
})},页面文本预览: ${pageText.slice(0, 2_000)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForInputbarKnowledgeHub(page, label, timeoutMs) {
|
||||
try {
|
||||
await page
|
||||
.locator('[data-testid="inputbar-knowledge-hub"]')
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
} catch (error) {
|
||||
const pageText = await page
|
||||
.locator("body")
|
||||
.innerText()
|
||||
.catch(() => "");
|
||||
const knowledgeControls = await page
|
||||
.locator("[data-testid^='inputbar-knowledge-']")
|
||||
.evaluateAll((items) =>
|
||||
items.slice(0, 40).map((item) => ({
|
||||
testId: item.getAttribute("data-testid"),
|
||||
text: (item.textContent || "").trim().replace(/\s+/g, " "),
|
||||
aria: item.getAttribute("aria-label"),
|
||||
title: item.getAttribute("title"),
|
||||
})),
|
||||
)
|
||||
.catch(() => []);
|
||||
throw new Error(
|
||||
`[smoke:knowledge-gui] ${label} 等待项目资料浮层失败 ${JSON.stringify({
|
||||
knowledgeControls,
|
||||
})},页面文本预览: ${pageText.slice(0, 2_000)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeFileManagerIfOpen(page) {
|
||||
const closeButton = page
|
||||
.getByRole("button", { name: "关闭文件管理器" })
|
||||
@@ -793,11 +885,25 @@ async function openKnowledgePageFromMainNav(page, options) {
|
||||
|
||||
throw new Error(
|
||||
`[smoke:knowledge-gui] 项目资料导航失败: ${
|
||||
lastError instanceof Error ? lastError.message : String(lastError)
|
||||
lastError instanceof Error ? lastError.message : String(lastError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function openHomeFromMainNav(page, options, label) {
|
||||
await syncSmokeProjectStorage(page, options);
|
||||
await clickScopedButton(page, {
|
||||
scope: '[data-testid="app-sidebar-main-nav"]',
|
||||
ariaLabel: "新建任务",
|
||||
});
|
||||
await waitForPageText(
|
||||
page,
|
||||
`${label} 新建任务页加载`,
|
||||
["青柠一下,灵感即来"],
|
||||
options.timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForKnowledgePack(options, label, matcher) {
|
||||
const startedAt = Date.now();
|
||||
let lastError = null;
|
||||
@@ -960,7 +1066,20 @@ async function runPlaywrightGuiFlow(options) {
|
||||
);
|
||||
|
||||
logStage("open-home-knowledge-hub");
|
||||
await clickPageControl(page, { text: "添加资料" });
|
||||
await waitForInputbarKnowledgePackToggle(
|
||||
page,
|
||||
"首页默认项目资料控件加载",
|
||||
options.timeoutMs,
|
||||
);
|
||||
await clickTestId(page, {
|
||||
testId: "entry-home-knowledge-import",
|
||||
label: "首页添加资料入口",
|
||||
});
|
||||
await waitForInputbarKnowledgeHub(
|
||||
page,
|
||||
"首页添加资料入口打开浮层",
|
||||
options.timeoutMs,
|
||||
);
|
||||
|
||||
logStage("wait-home-knowledge-hub");
|
||||
await waitForPageText(
|
||||
@@ -1037,16 +1156,7 @@ async function runPlaywrightGuiFlow(options) {
|
||||
options.timeoutMs,
|
||||
);
|
||||
await assertNoUserFacingInternalText(page, "项目资料状态说明页");
|
||||
await clickScopedButton(page, {
|
||||
scope: '[data-testid="app-sidebar-main-nav"]',
|
||||
ariaLabel: "灵感",
|
||||
});
|
||||
await waitForPageText(
|
||||
page,
|
||||
"灵感页加载",
|
||||
["收藏过的想法"],
|
||||
options.timeoutMs,
|
||||
);
|
||||
await openHomeFromMainNav(page, options, "状态说明后返回");
|
||||
await openKnowledgePageFromMainNav(page, options);
|
||||
await waitForPageText(
|
||||
page,
|
||||
@@ -1133,11 +1243,7 @@ async function runPlaywrightGuiFlow(options) {
|
||||
);
|
||||
|
||||
logStage("return-knowledge-overview");
|
||||
await clickScopedButton(page, {
|
||||
scope: '[data-testid="app-sidebar-main-nav"]',
|
||||
ariaLabel: "灵感",
|
||||
});
|
||||
await waitForPageText(page, "灵感页加载", ["灵感"], options.timeoutMs);
|
||||
await openHomeFromMainNav(page, options, "Agent 保存后返回");
|
||||
await openKnowledgePageFromMainNav(page, options);
|
||||
|
||||
logStage("wait-captured-agent-result");
|
||||
|
||||
Reference in New Issue
Block a user