mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: release v1.41.0
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
import { createAgentAppPackageHandoffReport } from "./lib/agent-app-package-handoff-core.mjs";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const result = {
|
||||
check: false,
|
||||
format: "summary",
|
||||
help: false,
|
||||
packageDir: "",
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if ((arg === "--package-dir" || arg === "--package") && argv[index + 1]) {
|
||||
result.packageDir = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--format" && argv[index + 1]) {
|
||||
result.format = String(argv[index + 1]).trim();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--check") {
|
||||
result.check = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
result.help = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Agent App Package Handoff Check
|
||||
|
||||
用法:
|
||||
node scripts/agent-app-package-handoff-check.mjs --package-dir <agent-app-package>
|
||||
node scripts/agent-app-package-handoff-check.mjs --package-dir <agent-app-package> --format json
|
||||
node scripts/agent-app-package-handoff-check.mjs --package-dir <agent-app-package> --check
|
||||
|
||||
选项:
|
||||
--package-dir PATH 要只读检查的 Agent App package 根目录。
|
||||
--format FMT summary | json,默认 summary。
|
||||
--check 若 package 仍 blocked / needs_handoff,则以非 0 退出。
|
||||
-h, --help 显示帮助。
|
||||
`);
|
||||
}
|
||||
|
||||
function readFileIfExists(filePath) {
|
||||
try {
|
||||
return {
|
||||
exists: fs.existsSync(filePath),
|
||||
content: fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "",
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
exists: false,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function collectGitStatusShort(packageDir) {
|
||||
try {
|
||||
return execFileSync("git", ["-C", packageDir, "status", "--short"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function walkFiles(rootDir) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
const entries = [];
|
||||
for (const name of fs.readdirSync(rootDir)) {
|
||||
const filePath = path.join(rootDir, name);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
entries.push(...walkFiles(filePath));
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile()) {
|
||||
entries.push(filePath);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function fileHash(filePath) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
function collectDistArtifacts(packageDir) {
|
||||
const entries = [];
|
||||
const pairs = [
|
||||
["src/ui", "dist/ui"],
|
||||
["src/core", "dist/core"],
|
||||
["src/integrations", "dist/integrations"],
|
||||
];
|
||||
|
||||
for (const [srcRootRelative, distRootRelative] of pairs) {
|
||||
const srcRoot = path.join(packageDir, srcRootRelative);
|
||||
const distRoot = path.join(packageDir, distRootRelative);
|
||||
for (const srcPath of walkFiles(srcRoot)) {
|
||||
const relativePath = path.relative(srcRoot, srcPath);
|
||||
const distPath = path.join(distRoot, relativePath);
|
||||
if (!fs.existsSync(distPath)) {
|
||||
entries.push({
|
||||
status: "missing-dist",
|
||||
src: path.relative(packageDir, srcPath),
|
||||
dist: path.relative(packageDir, distPath),
|
||||
srcHash: fileHash(srcPath),
|
||||
distHash: "",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const srcHash = fileHash(srcPath);
|
||||
const distHash = fileHash(distPath);
|
||||
entries.push({
|
||||
status: srcHash === distHash ? "same" : "diff",
|
||||
src: path.relative(packageDir, srcPath),
|
||||
dist: path.relative(packageDir, distPath),
|
||||
srcHash,
|
||||
distHash,
|
||||
});
|
||||
}
|
||||
|
||||
for (const distPath of walkFiles(distRoot)) {
|
||||
const relativePath = path.relative(distRoot, distPath);
|
||||
const srcPath = path.join(srcRoot, relativePath);
|
||||
if (!fs.existsSync(srcPath)) {
|
||||
entries.push({
|
||||
status: "extra-dist",
|
||||
src: path.relative(packageDir, srcPath),
|
||||
dist: path.relative(packageDir, distPath),
|
||||
srcHash: "",
|
||||
distHash: fileHash(distPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workerSrc = path.join(packageDir, "src", "worker", "index.mjs");
|
||||
const workerDist = path.join(packageDir, "dist", "worker", "index.mjs");
|
||||
if (fs.existsSync(workerSrc)) {
|
||||
if (!fs.existsSync(workerDist)) {
|
||||
entries.push({
|
||||
status: "missing-dist",
|
||||
src: path.relative(packageDir, workerSrc),
|
||||
dist: path.relative(packageDir, workerDist),
|
||||
srcHash: fileHash(workerSrc),
|
||||
distHash: "",
|
||||
});
|
||||
} else {
|
||||
const srcHash = fileHash(workerSrc);
|
||||
const distHash = fileHash(workerDist);
|
||||
entries.push({
|
||||
status: srcHash === distHash ? "same" : "diff",
|
||||
src: path.relative(packageDir, workerSrc),
|
||||
dist: path.relative(packageDir, workerDist),
|
||||
srcHash,
|
||||
distHash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function createReport(packageDir) {
|
||||
const resolvedPackageDir = path.resolve(packageDir);
|
||||
const hostBridgePath = path.join(resolvedPackageDir, "src", "ui", "host-bridge.js");
|
||||
const uiTestPath = path.join(resolvedPackageDir, "tests", "ui.test.mjs");
|
||||
const packageJsonPath = path.join(resolvedPackageDir, "package.json");
|
||||
const buildScriptPath = path.join(resolvedPackageDir, "scripts", "build.mjs");
|
||||
const packageJson = readFileIfExists(packageJsonPath);
|
||||
|
||||
return createAgentAppPackageHandoffReport({
|
||||
packageDir: resolvedPackageDir,
|
||||
gitStatusShort: collectGitStatusShort(resolvedPackageDir),
|
||||
files: {
|
||||
hostBridge: readFileIfExists(hostBridgePath),
|
||||
uiTest: readFileIfExists(uiTestPath),
|
||||
buildScript: readFileIfExists(buildScriptPath),
|
||||
distArtifacts: collectDistArtifacts(resolvedPackageDir),
|
||||
},
|
||||
packageJsonText: packageJson.content,
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarkerSummary(markers) {
|
||||
if (!markers.length) {
|
||||
return "none";
|
||||
}
|
||||
return markers.map((entry) => `${entry.marker}:${entry.count}`).join(", ");
|
||||
}
|
||||
|
||||
function renderSummary(report) {
|
||||
const lines = [
|
||||
`status=${report.verdict.status}`,
|
||||
`packageDir=${report.packageDir}`,
|
||||
`dirty=tracked:${report.gitStatus.trackedCount},untracked:${report.gitStatus.untrackedCount},total:${report.gitStatus.totalCount}`,
|
||||
`hostBridgePrivate=${renderMarkerSummary(report.files.hostBridge.privateMarkers)}`,
|
||||
`hostBridgeSdk=${renderMarkerSummary(report.files.hostBridge.sdkMarkers)}`,
|
||||
`uiTestPrivate=${renderMarkerSummary(report.files.uiTest.privateMarkers)}`,
|
||||
`highRiskScripts=${report.scripts.highRisk.map((entry) => entry.name).join(",") || "none"}`,
|
||||
`distArtifacts=diff:${report.distArtifacts.diffCount},missing:${report.distArtifacts.missingDistCount},extra:${report.distArtifacts.extraDistCount},total:${report.distArtifacts.totalDeltas}`,
|
||||
`blockers=${report.verdict.blockers.join(" | ") || "none"}`,
|
||||
`warnings=${report.verdict.warnings.join(" | ") || "none"}`,
|
||||
`nextAction=${report.verdict.nextAction}`,
|
||||
];
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
if (!args.packageDir) {
|
||||
console.error("缺少 --package-dir PATH。");
|
||||
printHelp();
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const report = createReport(args.packageDir);
|
||||
if (args.format === "json") {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
process.stdout.write(renderSummary(report));
|
||||
}
|
||||
|
||||
if (args.check && report.verdict.status !== "ready") {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -15,6 +15,12 @@ const DEFAULTS = {
|
||||
prefix: "agent-apps-smoke",
|
||||
};
|
||||
|
||||
const ACCOUNT_MENU_BUTTON_SELECTOR = '[data-testid="app-sidebar-account-button"]';
|
||||
const AGENT_APPS_NAV_SELECTOR =
|
||||
'button[aria-label="Agent Apps"], button[title="Agent Apps"]';
|
||||
const AGENT_APP_LAB_NAV_SELECTOR =
|
||||
'button[aria-label="Agent App Lab"], button[title="Agent App Lab"]';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -66,6 +72,21 @@ function logStage(stage) {
|
||||
console.log(`[smoke:agent-apps] stage=${stage}`);
|
||||
}
|
||||
|
||||
async function openAccountMenuForAgentApps(page, timeoutMs) {
|
||||
if ((await page.locator(AGENT_APPS_NAV_SELECTOR).count()) > 0) {
|
||||
return;
|
||||
}
|
||||
await page.click(ACCOUNT_MENU_BUTTON_SELECTOR);
|
||||
await page.waitForSelector(AGENT_APPS_NAV_SELECTOR, {
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickAgentAppsNav(page, timeoutMs) {
|
||||
await openAccountMenuForAgentApps(page, timeoutMs);
|
||||
await page.locator(AGENT_APPS_NAV_SELECTOR).first().click();
|
||||
}
|
||||
|
||||
async function launchSmokeContext(userDataDir) {
|
||||
try {
|
||||
return await chromium.launchPersistentContext(userDataDir, {
|
||||
@@ -127,8 +148,8 @@ function activeCloudBootstrapPayload() {
|
||||
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",
|
||||
packageHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
manifestHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
capabilityRequirements: {},
|
||||
defaultEntries: ["dashboard", "content_scenario_planning"],
|
||||
policyDefaults: {},
|
||||
@@ -158,10 +179,11 @@ async function runFlagOffRegression(options) {
|
||||
await page.waitForSelector('[data-testid="app-sidebar-main-nav"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await openAccountMenuForAgentApps(page, 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,
|
||||
agentAppsNavVisible: (await page.locator(AGENT_APPS_NAV_SELECTOR).count()) > 0,
|
||||
labNavHidden: (await page.locator(AGENT_APP_LAB_NAV_SELECTOR).count()) === 0,
|
||||
noConsoleErrors: consoleErrors.length === 0,
|
||||
};
|
||||
|
||||
@@ -200,6 +222,16 @@ async function main() {
|
||||
}, bootstrap);
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.route(
|
||||
"https://user.limeai.run/api/v1/public/tenants/*/client/agent-apps",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(bootstrap),
|
||||
});
|
||||
},
|
||||
);
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
@@ -219,17 +251,18 @@ async function main() {
|
||||
await page.waitForSelector('[data-testid="app-sidebar-main-nav"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await openAccountMenuForAgentApps(page, options.timeoutMs);
|
||||
assert(
|
||||
(await page.locator('button[title="Agent Apps"]').count()) > 0,
|
||||
(await page.locator(AGENT_APPS_NAV_SELECTOR).count()) > 0,
|
||||
"Agent Apps nav should be visible",
|
||||
);
|
||||
assert(
|
||||
(await page.locator('button[title="Agent App Lab"]').count()) === 0,
|
||||
(await page.locator(AGENT_APP_LAB_NAV_SELECTOR).count()) === 0,
|
||||
"Agent App Lab nav should stay hidden in formal smoke",
|
||||
);
|
||||
|
||||
logStage("open-agent-apps");
|
||||
await page.click('button[title="Agent Apps"]');
|
||||
await clickAgentAppsNav(page, options.timeoutMs);
|
||||
await page.waitForSelector('[data-testid="agent-apps-page"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
@@ -247,20 +280,30 @@ async function main() {
|
||||
|
||||
logStage("activate-bootstrap-catalog");
|
||||
await page.evaluate((payload) => {
|
||||
window.__LIME_BOOTSTRAP__ = payload;
|
||||
window.__LIME_OEM_CLOUD__ = {
|
||||
enabled: true,
|
||||
baseUrl: "https://user.limeai.run",
|
||||
tenantId: payload.tenantId,
|
||||
};
|
||||
window.__LIME_SESSION_TOKEN__ = "smoke-agent-apps-token";
|
||||
window.__LIME_BOOTSTRAP__ = { data: { agentAppCatalog: 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"),
|
||||
() => {
|
||||
const button = document.querySelector(
|
||||
'[data-testid="agent-apps-install-cloud-content-factory-app"]',
|
||||
);
|
||||
return button instanceof HTMLButtonElement && !button.disabled;
|
||||
},
|
||||
undefined,
|
||||
{ timeout: options.timeoutMs },
|
||||
);
|
||||
|
||||
logStage("install-cloud-review");
|
||||
await page.click('[data-testid="agent-apps-install-cloud-content-factory-app"]');
|
||||
await page.click('[data-testid="agent-apps-install-cloud-content-factory-app"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector('[data-testid="agent-apps-install-review"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
@@ -308,7 +351,7 @@ async function main() {
|
||||
);
|
||||
|
||||
logStage("return-agent-apps");
|
||||
await page.click('button[title="Agent Apps"]');
|
||||
await clickAgentAppsNav(page, options.timeoutMs);
|
||||
await page.waitForSelector('[data-testid="agent-apps-page"]', {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
@@ -348,7 +391,9 @@ async function main() {
|
||||
runtimeSurfaceVisible: Boolean(runtimeFrameSrc),
|
||||
cleanupEvidenceSelectedApp: cleanupEvidence.appId === "content-factory-app",
|
||||
cleanupEvidenceStrategy: cleanupEvidence.strategy === "delete-data",
|
||||
cleanupEvidenceRehearsalOnly: cleanupEvidence.completionEffect === "rehearsal-only",
|
||||
cleanupEvidenceDryRunOnly:
|
||||
Array.isArray(cleanupEvidence.warningCodes) &&
|
||||
cleanupEvidence.warningCodes.includes("DRY_RUN_ONLY"),
|
||||
cleanupEvidenceBlockedCount: cleanupEvidence.blockedTargetCount === 0,
|
||||
flagOffAgentAppsNavVisible: flagOff.assertions.agentAppsNavVisible,
|
||||
flagOffLabNavHidden: flagOff.assertions.labNavHidden,
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
const PRIVATE_BRIDGE_MARKERS = [
|
||||
"pendingRequests",
|
||||
"buildMessage",
|
||||
"requestHostBridge",
|
||||
"window.parent.postMessage",
|
||||
"capability:invoke",
|
||||
"capability:subscribe",
|
||||
"capability:event",
|
||||
];
|
||||
|
||||
const SDK_FACADE_MARKERS = [
|
||||
"createLimeHostBridgeCapabilityInvoker",
|
||||
"createLimeCoreCapabilityAdapters",
|
||||
];
|
||||
|
||||
const HIGH_RISK_SCRIPT_PATTERNS = [
|
||||
{
|
||||
scriptName: "build",
|
||||
pattern: /\brm\s*\(|\brimraf\b|rm\s+-rf\s+dist|remove\s*\(\s*["']dist["']/i,
|
||||
reason: "build script may delete or rewrite dist artifacts",
|
||||
},
|
||||
{
|
||||
scriptName: "verify",
|
||||
pattern: /\bnpm\s+run\s+build\b|\bpnpm\s+run\s+build\b|\byarn\s+build\b/i,
|
||||
reason: "verify script runs build before validation",
|
||||
},
|
||||
{
|
||||
scriptName: "e2e",
|
||||
pattern: /\bnpm\s+run\s+build\b|\bpnpm\s+run\s+build\b|\byarn\s+build\b/i,
|
||||
reason: "e2e script runs build before user-flow validation",
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || "");
|
||||
}
|
||||
|
||||
function countMarkerHits(text, markers) {
|
||||
const content = normalizeText(text);
|
||||
return markers
|
||||
.map((marker) => ({
|
||||
marker,
|
||||
count: content.split(marker).length - 1,
|
||||
}))
|
||||
.filter((entry) => entry.count > 0);
|
||||
}
|
||||
|
||||
function parseGitStatusShort(output) {
|
||||
const entries = normalizeText(output)
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter(Boolean)
|
||||
.map((line) => ({
|
||||
raw: line,
|
||||
status: line.slice(0, 2),
|
||||
path: line.slice(3).trim(),
|
||||
untracked: line.startsWith("??"),
|
||||
}));
|
||||
|
||||
const tracked = entries.filter((entry) => !entry.untracked);
|
||||
const untracked = entries.filter((entry) => entry.untracked);
|
||||
return {
|
||||
entries,
|
||||
trackedCount: tracked.length,
|
||||
untrackedCount: untracked.length,
|
||||
totalCount: entries.length,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePackageJson(packageJsonText) {
|
||||
if (!normalizeText(packageJsonText).trim()) {
|
||||
return { ok: false, scripts: {}, error: "package_json_missing" };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(packageJsonText);
|
||||
return {
|
||||
ok: true,
|
||||
scripts: parsed?.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {},
|
||||
error: "",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
scripts: {},
|
||||
error: `package_json_invalid:${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function analyzeScripts(scripts, scriptFiles = {}) {
|
||||
const entries = Object.entries(scripts || {}).map(([name, command]) => ({
|
||||
name,
|
||||
command: normalizeText(command),
|
||||
}));
|
||||
const highRisk = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
for (const rule of HIGH_RISK_SCRIPT_PATTERNS) {
|
||||
const nameMatches =
|
||||
rule.scriptName === "e2e"
|
||||
? entry.name.toLowerCase().includes("e2e")
|
||||
: entry.name === rule.scriptName;
|
||||
const inspectedText =
|
||||
entry.name === "build"
|
||||
? `${entry.command}\n${normalizeText(scriptFiles.build)}`
|
||||
: entry.command;
|
||||
if (nameMatches && rule.pattern.test(inspectedText)) {
|
||||
highRisk.push({
|
||||
name: entry.name,
|
||||
command: entry.command,
|
||||
reason: rule.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entries,
|
||||
highRisk,
|
||||
};
|
||||
}
|
||||
|
||||
function createFileMarkerReport({ exists, content }) {
|
||||
return {
|
||||
exists: Boolean(exists),
|
||||
privateMarkers: countMarkerHits(content, PRIVATE_BRIDGE_MARKERS),
|
||||
sdkMarkers: countMarkerHits(content, SDK_FACADE_MARKERS),
|
||||
};
|
||||
}
|
||||
|
||||
function createDistArtifactReport(entries = []) {
|
||||
const normalizedEntries = entries.map((entry) => ({
|
||||
status: normalizeText(entry.status),
|
||||
src: normalizeText(entry.src),
|
||||
dist: normalizeText(entry.dist),
|
||||
srcHash: normalizeText(entry.srcHash),
|
||||
distHash: normalizeText(entry.distHash),
|
||||
}));
|
||||
const deltas = normalizedEntries.filter((entry) => entry.status !== "same");
|
||||
|
||||
return {
|
||||
entries: normalizedEntries,
|
||||
deltas,
|
||||
totalDeltas: deltas.length,
|
||||
diffCount: deltas.filter((entry) => entry.status === "diff").length,
|
||||
missingDistCount: deltas.filter((entry) => entry.status === "missing-dist").length,
|
||||
extraDistCount: deltas.filter((entry) => entry.status === "extra-dist").length,
|
||||
};
|
||||
}
|
||||
|
||||
function createVerdict({ gitStatus, hostBridge, uiTest, scripts, distArtifacts }) {
|
||||
const blockers = [];
|
||||
const warnings = [];
|
||||
|
||||
if (!hostBridge.exists) {
|
||||
blockers.push("src/ui/host-bridge.js is missing");
|
||||
}
|
||||
if (!uiTest.exists) {
|
||||
warnings.push("tests/ui.test.mjs is missing; package-side bridge regression is not visible");
|
||||
}
|
||||
|
||||
if (hostBridge.privateMarkers.length > 0 || uiTest.privateMarkers.length > 0) {
|
||||
blockers.push("private Host Bridge transport markers are still present");
|
||||
}
|
||||
if (hostBridge.sdkMarkers.length === 0) {
|
||||
blockers.push("SDK facade markers are not present in src/ui/host-bridge.js");
|
||||
}
|
||||
if (gitStatus.totalCount > 0) {
|
||||
warnings.push(
|
||||
`package worktree is dirty: tracked=${gitStatus.trackedCount}, untracked=${gitStatus.untrackedCount}`,
|
||||
);
|
||||
}
|
||||
if (scripts.highRisk.length > 0) {
|
||||
warnings.push("build/verify/e2e scripts may rewrite dist artifacts");
|
||||
}
|
||||
if (distArtifacts.totalDeltas > 0) {
|
||||
warnings.push(`dist artifacts are not synchronized: ${distArtifacts.totalDeltas} delta(s)`);
|
||||
}
|
||||
|
||||
const status = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "needs_handoff" : "ready";
|
||||
const nextAction =
|
||||
status === "ready"
|
||||
? "Run package tests and the agreed package verify gate."
|
||||
: status === "needs_handoff"
|
||||
? "Confirm owner handoff before changing or rebuilding package artifacts."
|
||||
: "Do not claim P18.5.3 complete; remove private bridge transport through the SDK facade first.";
|
||||
|
||||
return {
|
||||
status,
|
||||
blockers,
|
||||
warnings,
|
||||
nextAction,
|
||||
};
|
||||
}
|
||||
|
||||
function createAgentAppPackageHandoffReport({
|
||||
generatedAt = new Date().toISOString(),
|
||||
packageDir = "",
|
||||
gitStatusShort = "",
|
||||
files = {},
|
||||
packageJsonText = "",
|
||||
} = {}) {
|
||||
const gitStatus = parseGitStatusShort(gitStatusShort);
|
||||
const hostBridge = createFileMarkerReport(files.hostBridge || {});
|
||||
const uiTest = createFileMarkerReport(files.uiTest || {});
|
||||
const packageJson = parsePackageJson(packageJsonText);
|
||||
const scripts = analyzeScripts(packageJson.scripts, {
|
||||
build: files.buildScript?.content,
|
||||
});
|
||||
const distArtifacts = createDistArtifactReport(files.distArtifacts || []);
|
||||
const verdict = createVerdict({ gitStatus, hostBridge, uiTest, scripts, distArtifacts });
|
||||
|
||||
return {
|
||||
schemaVersion: "v1",
|
||||
generatedAt,
|
||||
packageDir,
|
||||
gitStatus,
|
||||
files: {
|
||||
hostBridge,
|
||||
uiTest,
|
||||
},
|
||||
packageJson,
|
||||
scripts,
|
||||
distArtifacts,
|
||||
verdict,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
PRIVATE_BRIDGE_MARKERS,
|
||||
SDK_FACADE_MARKERS,
|
||||
analyzeScripts,
|
||||
createAgentAppPackageHandoffReport,
|
||||
createDistArtifactReport,
|
||||
countMarkerHits,
|
||||
parseGitStatusShort,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
analyzeScripts,
|
||||
createAgentAppPackageHandoffReport,
|
||||
createDistArtifactReport,
|
||||
countMarkerHits,
|
||||
parseGitStatusShort,
|
||||
} from "./agent-app-package-handoff-core.mjs";
|
||||
|
||||
describe("agent-app-package-handoff-core", () => {
|
||||
it("统计 git status 的 tracked / untracked 条目", () => {
|
||||
const status = parseGitStatusShort(" M src/ui/app.js\n?? src/ui/host-bridge.js\n");
|
||||
|
||||
expect(status.trackedCount).toBe(1);
|
||||
expect(status.untrackedCount).toBe(1);
|
||||
expect(status.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it("识别私有 Host Bridge marker 与 SDK marker", () => {
|
||||
const markers = countMarkerHits(
|
||||
"pendingRequests requestHostBridge createLimeHostBridgeCapabilityInvoker",
|
||||
["pendingRequests", "requestHostBridge", "createLimeHostBridgeCapabilityInvoker"],
|
||||
);
|
||||
|
||||
expect(markers.map((entry) => entry.marker)).toEqual([
|
||||
"pendingRequests",
|
||||
"requestHostBridge",
|
||||
"createLimeHostBridgeCapabilityInvoker",
|
||||
]);
|
||||
});
|
||||
|
||||
it("把仍保留私有 bridge transport 的 package 判为 blocked", () => {
|
||||
const report = createAgentAppPackageHandoffReport({
|
||||
generatedAt: "2026-05-16T00:00:00.000Z",
|
||||
packageDir: "/tmp/content-factory-app",
|
||||
gitStatusShort: " M src/ui/app.js\n?? src/ui/host-bridge.js\n",
|
||||
files: {
|
||||
hostBridge: {
|
||||
exists: true,
|
||||
content: "const pendingRequests = new Map(); window.parent.postMessage({ type: 'capability:invoke' });",
|
||||
},
|
||||
uiTest: {
|
||||
exists: true,
|
||||
content: "message.type === 'capability:invoke'",
|
||||
},
|
||||
},
|
||||
packageJsonText: JSON.stringify({
|
||||
scripts: {
|
||||
test: "node --test tests/*.test.mjs",
|
||||
verify: "npm run build && npm run test",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(report.verdict.status).toBe("blocked");
|
||||
expect(report.verdict.blockers).toContain("private Host Bridge transport markers are still present");
|
||||
expect(report.verdict.warnings).toContain("package worktree is dirty: tracked=1, untracked=1");
|
||||
});
|
||||
|
||||
it("无私有 marker 但 dirty 时要求 handoff", () => {
|
||||
const report = createAgentAppPackageHandoffReport({
|
||||
gitStatusShort: " M src/ui/host-bridge.js\n",
|
||||
files: {
|
||||
hostBridge: {
|
||||
exists: true,
|
||||
content: "createLimeHostBridgeCapabilityInvoker(); createLimeCoreCapabilityAdapters();",
|
||||
},
|
||||
uiTest: {
|
||||
exists: true,
|
||||
content: "sdkInvokerCallLog",
|
||||
},
|
||||
},
|
||||
packageJsonText: JSON.stringify({
|
||||
scripts: {
|
||||
test: "node --test tests/*.test.mjs",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(report.verdict.status).toBe("needs_handoff");
|
||||
expect(report.verdict.blockers).toEqual([]);
|
||||
});
|
||||
|
||||
it("识别会重建 dist 的 build / verify / e2e 脚本", () => {
|
||||
const scripts = analyzeScripts(
|
||||
{
|
||||
build: "node scripts/build.mjs",
|
||||
verify: "npm run build && npm run test",
|
||||
"e2e:user-flow": "npm run build && playwright test",
|
||||
},
|
||||
{
|
||||
build: "await rm('dist', { recursive: true, force: true });",
|
||||
},
|
||||
);
|
||||
|
||||
expect(scripts.highRisk.map((entry) => entry.name)).toEqual([
|
||||
"build",
|
||||
"verify",
|
||||
"e2e:user-flow",
|
||||
]);
|
||||
});
|
||||
|
||||
it("统计 src / dist 产物漂移并作为 handoff warning", () => {
|
||||
const distArtifacts = createDistArtifactReport([
|
||||
{
|
||||
status: "same",
|
||||
src: "src/ui/app.js",
|
||||
dist: "dist/ui/app.js",
|
||||
srcHash: "aaa",
|
||||
distHash: "aaa",
|
||||
},
|
||||
{
|
||||
status: "diff",
|
||||
src: "src/ui/host-bridge.js",
|
||||
dist: "dist/ui/host-bridge.js",
|
||||
srcHash: "bbb",
|
||||
distHash: "ccc",
|
||||
},
|
||||
{
|
||||
status: "missing-dist",
|
||||
src: "src/ui/lime-app-sdk.js",
|
||||
dist: "dist/ui/lime-app-sdk.js",
|
||||
srcHash: "ddd",
|
||||
distHash: "",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(distArtifacts.totalDeltas).toBe(2);
|
||||
expect(distArtifacts.diffCount).toBe(1);
|
||||
expect(distArtifacts.missingDistCount).toBe(1);
|
||||
|
||||
const report = createAgentAppPackageHandoffReport({
|
||||
files: {
|
||||
hostBridge: {
|
||||
exists: true,
|
||||
content: "createLimeHostBridgeCapabilityInvoker(); createLimeCoreCapabilityAdapters();",
|
||||
},
|
||||
uiTest: {
|
||||
exists: true,
|
||||
content: "sdkInvokerCallLog",
|
||||
},
|
||||
distArtifacts: distArtifacts.entries,
|
||||
},
|
||||
packageJsonText: JSON.stringify({
|
||||
scripts: {
|
||||
test: "node --test tests/*.test.mjs",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(report.verdict.status).toBe("needs_handoff");
|
||||
expect(report.verdict.warnings).toContain("dist artifacts are not synchronized: 2 delta(s)");
|
||||
});
|
||||
});
|
||||
@@ -1591,6 +1591,25 @@ async function main() {
|
||||
options.timeoutMs + 30_000,
|
||||
);
|
||||
|
||||
await runCommand(
|
||||
npmCommand,
|
||||
[
|
||||
"run",
|
||||
"smoke:agent-apps",
|
||||
"--",
|
||||
"--app-url",
|
||||
options.appUrl,
|
||||
"--health-url",
|
||||
options.healthUrl,
|
||||
"--timeout-ms",
|
||||
String(options.timeoutMs),
|
||||
"--interval-ms",
|
||||
String(options.intervalMs),
|
||||
],
|
||||
"smoke:agent-apps",
|
||||
options.timeoutMs + 30_000,
|
||||
);
|
||||
|
||||
await runCommand(
|
||||
npmCommand,
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user