mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: prepare v1.28.0 release
This commit is contained in:
@@ -361,6 +361,9 @@ function main() {
|
||||
const runtimeGatewayCommands = new Set(
|
||||
agentCommandCatalog.runtimeGatewayCommands ?? [],
|
||||
);
|
||||
const capabilityDraftCommands = new Set(
|
||||
agentCommandCatalog.capabilityDraftCommands ?? [],
|
||||
);
|
||||
|
||||
const deferredCommands = new Set(knownDeferredRegistrationReasons.keys());
|
||||
|
||||
@@ -390,6 +393,12 @@ function main() {
|
||||
!registeredCommands.has(command) && !deferredCommands.has(command),
|
||||
),
|
||||
);
|
||||
const capabilityDraftMissingRegistrations = new Set(
|
||||
[...capabilityDraftCommands].filter(
|
||||
(command) =>
|
||||
!registeredCommands.has(command) && !deferredCommands.has(command),
|
||||
),
|
||||
);
|
||||
|
||||
console.log("[command-contracts] frontend commands:", frontendCommands.size);
|
||||
console.log(
|
||||
@@ -456,6 +465,14 @@ function main() {
|
||||
);
|
||||
}
|
||||
|
||||
if (capabilityDraftMissingRegistrations.size > 0) {
|
||||
hasError = true;
|
||||
printCommandGroup(
|
||||
"capability draft 命令缺少 Rust 注册",
|
||||
capabilityDraftMissingRegistrations,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,12 @@ const CAPABILITY_MATRIX_PATH =
|
||||
const ARTIFACT_GRAPH_PATH = "src/lib/governance/modalityArtifactGraph.json";
|
||||
const EXECUTION_PROFILE_PATH =
|
||||
"src/lib/governance/modalityExecutionProfiles.json";
|
||||
const TASK_INDEX_PRESENTATION_PATH =
|
||||
"src/lib/agentRuntime/modalityTaskIndexPresentation.ts";
|
||||
const HARNESS_TASK_INDEX_SECTION_PATH =
|
||||
"src/components/agent/chat/components/HarnessTaskIndexSection.tsx";
|
||||
const HARNESS_STATUS_PANEL_PATH =
|
||||
"src/components/agent/chat/components/HarnessStatusPanel.tsx";
|
||||
const REQUIRED_DOCS = [
|
||||
"docs/roadmap/warp/runtime-fact-map.md",
|
||||
"docs/roadmap/warp/contract-schema.md",
|
||||
@@ -20,6 +26,11 @@ const REQUIRED_DOCS = [
|
||||
"docs/roadmap/warp/task-index-inventory.md",
|
||||
"docs/roadmap/warp/evolution-guide.md",
|
||||
];
|
||||
const REQUIRED_TASK_INDEX_PRESENTATION_EXPORTS = [
|
||||
"buildModalityTaskIndexFacets",
|
||||
"buildModalityTaskIndexRows",
|
||||
"filterModalityTaskIndexRows",
|
||||
];
|
||||
|
||||
const LIFECYCLES = new Set(["current", "compat", "deprecated", "dead"]);
|
||||
const MODALITIES = new Set([
|
||||
@@ -132,6 +143,30 @@ const REQUIRED_ARTIFACT_INDEX_FIELDS = new Set([
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
const MEDIA_TASK_ARTIFACT_KINDS_REQUIRING_PHASE8_FIELDS = new Set([
|
||||
"image_task",
|
||||
"image_output",
|
||||
"audio_task",
|
||||
"audio_output",
|
||||
"transcript",
|
||||
]);
|
||||
const REQUIRED_MEDIA_TASK_PHASE8_INDEX_FIELDS = new Set([
|
||||
"entry_key",
|
||||
"thread_id",
|
||||
"turn_id",
|
||||
"content_id",
|
||||
"modality",
|
||||
"skill_id",
|
||||
"model_id",
|
||||
"cost_state",
|
||||
"limit_state",
|
||||
"estimated_cost_class",
|
||||
"limit_event_kind",
|
||||
"quota_low",
|
||||
"executor_kind",
|
||||
"executor_binding_key",
|
||||
"limecore_policy_snapshot_status",
|
||||
]);
|
||||
const ENTRY_KINDS = new Set([
|
||||
"command",
|
||||
"button_action",
|
||||
@@ -158,7 +193,11 @@ const PHASE7_REQUIRED_ENTRY_BINDINGS = new Map([
|
||||
|
||||
function collectUniqueObjects(errors, collection, keyName, label) {
|
||||
const result = new Map();
|
||||
pushIf(errors, !isNonEmptyArray(collection), `${label} must be a non-empty array`);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyArray(collection),
|
||||
`${label} must be a non-empty array`,
|
||||
);
|
||||
if (!Array.isArray(collection)) {
|
||||
return result;
|
||||
}
|
||||
@@ -179,7 +218,11 @@ function collectUniqueObjects(errors, collection, keyName, label) {
|
||||
if (!isNonEmptyString(key)) {
|
||||
return;
|
||||
}
|
||||
pushIf(errors, result.has(key), `${prefix}.${keyName} is duplicated: ${key}`);
|
||||
pushIf(
|
||||
errors,
|
||||
result.has(key),
|
||||
`${prefix}.${keyName} is duplicated: ${key}`,
|
||||
);
|
||||
result.set(key, item);
|
||||
});
|
||||
|
||||
@@ -209,7 +252,13 @@ function pushIf(errors, condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnumArray(errors, contractKey, fieldName, values, allowedValues) {
|
||||
function validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
fieldName,
|
||||
values,
|
||||
allowedValues,
|
||||
) {
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyArray(values),
|
||||
@@ -228,9 +277,17 @@ function validateEnumArray(errors, contractKey, fieldName, values, allowedValues
|
||||
}
|
||||
}
|
||||
|
||||
function validateStringArray(errors, contractKey, fieldName, values, options = {}) {
|
||||
function validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
fieldName,
|
||||
values,
|
||||
options = {},
|
||||
) {
|
||||
const { allowEmpty = false } = options;
|
||||
const invalidArray = allowEmpty ? !Array.isArray(values) : !isNonEmptyArray(values);
|
||||
const invalidArray = allowEmpty
|
||||
? !Array.isArray(values)
|
||||
: !isNonEmptyArray(values);
|
||||
pushIf(
|
||||
errors,
|
||||
invalidArray,
|
||||
@@ -435,7 +492,10 @@ function validatePhase7EntryBindingCoverage(errors, registry) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const [contractKey, requiredEntryKeys] of PHASE7_REQUIRED_ENTRY_BINDINGS) {
|
||||
for (const [
|
||||
contractKey,
|
||||
requiredEntryKeys,
|
||||
] of PHASE7_REQUIRED_ENTRY_BINDINGS) {
|
||||
const contract = registry.contracts.find(
|
||||
(candidate) =>
|
||||
isPlainObject(candidate) && candidate.contract_key === contractKey,
|
||||
@@ -544,13 +604,9 @@ function validateCapabilityMatrix(matrix) {
|
||||
role.capability_keys,
|
||||
new Set(capabilityMap.keys()),
|
||||
);
|
||||
validateStringArray(
|
||||
errors,
|
||||
slot,
|
||||
"fallback_slots",
|
||||
role.fallback_slots,
|
||||
{ allowEmpty: true },
|
||||
);
|
||||
validateStringArray(errors, slot, "fallback_slots", role.fallback_slots, {
|
||||
allowEmpty: true,
|
||||
});
|
||||
if (Array.isArray(role.fallback_slots)) {
|
||||
for (const fallbackSlot of role.fallback_slots) {
|
||||
pushIf(
|
||||
@@ -683,6 +739,15 @@ function validateArtifactGraph(graph) {
|
||||
`artifact ${kind}.task_index_fields must include ${requiredField}`,
|
||||
);
|
||||
}
|
||||
if (MEDIA_TASK_ARTIFACT_KINDS_REQUIRING_PHASE8_FIELDS.has(kind)) {
|
||||
for (const requiredField of REQUIRED_MEDIA_TASK_PHASE8_INDEX_FIELDS) {
|
||||
pushIf(
|
||||
errors,
|
||||
!indexFields.has(requiredField),
|
||||
`artifact ${kind}.task_index_fields must include ${requiredField} for Phase 8 media task index projection`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,7 +774,9 @@ function validateContractArtifactGraph(errors, contract, artifactGraphRefs) {
|
||||
for (const artifactKind of contract.artifact_kinds) {
|
||||
const artifact = artifactGraphRefs.artifactMap.get(artifactKind);
|
||||
if (!artifact) {
|
||||
errors.push(`${contractKey}.artifact_kinds references artifact not defined in graph: ${artifactKind}`);
|
||||
errors.push(
|
||||
`${contractKey}.artifact_kinds references artifact not defined in graph: ${artifactKind}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -742,7 +809,10 @@ function resolveExecutorAdapterKey(executor) {
|
||||
if (!isPlainObject(executor)) {
|
||||
return null;
|
||||
}
|
||||
if (!isNonEmptyString(executor.executor_kind) || !isNonEmptyString(executor.binding_key)) {
|
||||
if (
|
||||
!isNonEmptyString(executor.executor_kind) ||
|
||||
!isNonEmptyString(executor.binding_key)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `${executor.executor_kind}:${executor.binding_key}`;
|
||||
@@ -760,7 +830,11 @@ function validateSubset(errors, label, actualValues, requiredValues) {
|
||||
}
|
||||
|
||||
function validateArtifactPolicy(errors, label, policy, artifactGraphRefs) {
|
||||
pushIf(errors, !isPlainObject(policy), `${label}.artifact_policy must be an object`);
|
||||
pushIf(
|
||||
errors,
|
||||
!isPlainObject(policy),
|
||||
`${label}.artifact_policy must be an object`,
|
||||
);
|
||||
if (!isPlainObject(policy)) {
|
||||
return;
|
||||
}
|
||||
@@ -794,7 +868,12 @@ function validateArtifactPolicy(errors, label, policy, artifactGraphRefs) {
|
||||
);
|
||||
}
|
||||
|
||||
function validateExecutionProfiles(registry, profiles, matrixRefs, artifactGraphRefs) {
|
||||
function validateExecutionProfiles(
|
||||
registry,
|
||||
profiles,
|
||||
matrixRefs,
|
||||
artifactGraphRefs,
|
||||
) {
|
||||
const errors = [];
|
||||
pushIf(errors, profiles.version !== 1, "executionProfiles.version must be 1");
|
||||
pushIf(
|
||||
@@ -946,7 +1025,12 @@ function validateExecutionProfiles(registry, profiles, matrixRefs, artifactGraph
|
||||
profile.executor_adapter_keys,
|
||||
new Set(adapterMap.keys()),
|
||||
);
|
||||
validateArtifactPolicy(errors, profileKey, profile.artifact_policy, artifactGraphRefs);
|
||||
validateArtifactPolicy(
|
||||
errors,
|
||||
profileKey,
|
||||
profile.artifact_policy,
|
||||
artifactGraphRefs,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
profileKey,
|
||||
@@ -972,7 +1056,12 @@ function validateExecutionProfiles(registry, profiles, matrixRefs, artifactGraph
|
||||
profile.evidence_events,
|
||||
EVIDENCE_EVENTS,
|
||||
);
|
||||
validateStringArray(errors, profileKey, "audit_fields", profile.audit_fields);
|
||||
validateStringArray(
|
||||
errors,
|
||||
profileKey,
|
||||
"audit_fields",
|
||||
profile.audit_fields,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyString(profile.notes),
|
||||
@@ -1094,9 +1183,21 @@ function validateExecutionProfiles(registry, profiles, matrixRefs, artifactGraph
|
||||
function validateContractRegistry(registry, matrixRefs, artifactGraphRefs) {
|
||||
const errors = [];
|
||||
pushIf(errors, registry.version !== 1, "registry.version must be 1");
|
||||
pushIf(errors, registry.status !== "current", "registry.status must be current");
|
||||
pushIf(errors, !isNonEmptyString(registry.owner), "registry.owner must be set");
|
||||
pushIf(errors, !isNonEmptyArray(registry.contracts), "registry.contracts must be a non-empty array");
|
||||
pushIf(
|
||||
errors,
|
||||
registry.status !== "current",
|
||||
"registry.status must be current",
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyString(registry.owner),
|
||||
"registry.owner must be set",
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyArray(registry.contracts),
|
||||
"registry.contracts must be a non-empty array",
|
||||
);
|
||||
if (!Array.isArray(registry.contracts)) {
|
||||
return errors;
|
||||
}
|
||||
@@ -1116,7 +1217,8 @@ function validateContractRegistry(registry, matrixRefs, artifactGraphRefs) {
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
isNonEmptyString(contract.contract_key) && looksLikeEntryKey(contract.contract_key),
|
||||
isNonEmptyString(contract.contract_key) &&
|
||||
looksLikeEntryKey(contract.contract_key),
|
||||
`${contractKey}.contract_key must be a bottom-layer key, not an entry key`,
|
||||
);
|
||||
pushIf(
|
||||
@@ -1136,8 +1238,18 @@ function validateContractRegistry(registry, matrixRefs, artifactGraphRefs) {
|
||||
!MODALITIES.has(contract.modality),
|
||||
`${contractKey}.modality is unknown: ${String(contract.modality)}`,
|
||||
);
|
||||
validateStringArray(errors, contractKey, "runtime_identity", contract.runtime_identity);
|
||||
validateStringArray(errors, contractKey, "input_context_kinds", contract.input_context_kinds);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"runtime_identity",
|
||||
contract.runtime_identity,
|
||||
);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"input_context_kinds",
|
||||
contract.input_context_kinds,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
@@ -1145,8 +1257,18 @@ function validateContractRegistry(registry, matrixRefs, artifactGraphRefs) {
|
||||
contract.required_capabilities,
|
||||
matrixRefs.capabilityKeys,
|
||||
);
|
||||
validateEnumArray(errors, contractKey, "permission_profile_keys", contract.permission_profile_keys, PERMISSIONS);
|
||||
pushIf(errors, !isNonEmptyString(contract.routing_slot), `${contractKey}.routing_slot must be set`);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"permission_profile_keys",
|
||||
contract.permission_profile_keys,
|
||||
PERMISSIONS,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyString(contract.routing_slot),
|
||||
`${contractKey}.routing_slot must be set`,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
isNonEmptyString(contract.routing_slot) &&
|
||||
@@ -1154,19 +1276,72 @@ function validateContractRegistry(registry, matrixRefs, artifactGraphRefs) {
|
||||
`${contractKey}.routing_slot is not defined in capability matrix: ${String(contract.routing_slot)}`,
|
||||
);
|
||||
validateExecutor(errors, contract);
|
||||
validateStringArray(errors, contractKey, "truth_source", contract.truth_source);
|
||||
validateEnumArray(errors, contractKey, "artifact_kinds", contract.artifact_kinds, ARTIFACT_KINDS);
|
||||
validateEnumArray(errors, contractKey, "viewer_surface", contract.viewer_surface, VIEWER_SURFACES);
|
||||
validateEnumArray(errors, contractKey, "evidence_events", contract.evidence_events, EVIDENCE_EVENTS);
|
||||
validateEnumArray(errors, contractKey, "limecore_policy_refs", contract.limecore_policy_refs, LIMECORE_POLICY_REFS);
|
||||
validateStringArray(errors, contractKey, "fallback_policy", contract.fallback_policy);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"truth_source",
|
||||
contract.truth_source,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"artifact_kinds",
|
||||
contract.artifact_kinds,
|
||||
ARTIFACT_KINDS,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"viewer_surface",
|
||||
contract.viewer_surface,
|
||||
VIEWER_SURFACES,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"evidence_events",
|
||||
contract.evidence_events,
|
||||
EVIDENCE_EVENTS,
|
||||
);
|
||||
validateEnumArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"limecore_policy_refs",
|
||||
contract.limecore_policy_refs,
|
||||
LIMECORE_POLICY_REFS,
|
||||
);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"fallback_policy",
|
||||
contract.fallback_policy,
|
||||
);
|
||||
|
||||
pushIf(errors, !isPlainObject(contract.detour_policy), `${contractKey}.detour_policy must be an object`);
|
||||
pushIf(
|
||||
errors,
|
||||
!isPlainObject(contract.detour_policy),
|
||||
`${contractKey}.detour_policy must be an object`,
|
||||
);
|
||||
if (isPlainObject(contract.detour_policy)) {
|
||||
validateStringArray(errors, contractKey, "detour_policy.allowed", contract.detour_policy.allowed, { allowEmpty: true });
|
||||
validateStringArray(errors, contractKey, "detour_policy.denied", contract.detour_policy.denied);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"detour_policy.allowed",
|
||||
contract.detour_policy.allowed,
|
||||
{ allowEmpty: true },
|
||||
);
|
||||
validateStringArray(
|
||||
errors,
|
||||
contractKey,
|
||||
"detour_policy.denied",
|
||||
contract.detour_policy.denied,
|
||||
);
|
||||
}
|
||||
pushIf(errors, !isNonEmptyString(contract.owner_surface), `${contractKey}.owner_surface must be set`);
|
||||
pushIf(
|
||||
errors,
|
||||
!isNonEmptyString(contract.owner_surface),
|
||||
`${contractKey}.owner_surface must be set`,
|
||||
);
|
||||
validateEntryBindings(errors, contract);
|
||||
validateContractArtifactGraph(errors, contract, artifactGraphRefs);
|
||||
}
|
||||
@@ -1187,7 +1362,86 @@ function validateRequiredDocs() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderSuccess(registry, matrix, graph, contractReport, profileReport) {
|
||||
function readRequiredTextFile(errors, filePath) {
|
||||
const absolutePath = path.resolve(process.cwd(), filePath);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
errors.push(`required task index source file is missing: ${filePath}`);
|
||||
return "";
|
||||
}
|
||||
return fs.readFileSync(absolutePath, "utf8");
|
||||
}
|
||||
|
||||
function validateTaskIndexPresentationGuard() {
|
||||
const errors = [];
|
||||
const presentationSource = readRequiredTextFile(
|
||||
errors,
|
||||
TASK_INDEX_PRESENTATION_PATH,
|
||||
);
|
||||
const sectionSource = readRequiredTextFile(
|
||||
errors,
|
||||
HARNESS_TASK_INDEX_SECTION_PATH,
|
||||
);
|
||||
const panelSource = readRequiredTextFile(errors, HARNESS_STATUS_PANEL_PATH);
|
||||
|
||||
for (const exportName of REQUIRED_TASK_INDEX_PRESENTATION_EXPORTS) {
|
||||
pushIf(
|
||||
errors,
|
||||
!presentationSource.includes(`export function ${exportName}`),
|
||||
`${TASK_INDEX_PRESENTATION_PATH} must export ${exportName} as the Phase 8 taskIndex query fact source`,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!sectionSource.includes(exportName),
|
||||
`${HARNESS_TASK_INDEX_SECTION_PATH} must consume ${exportName} instead of rebuilding taskIndex UI state`,
|
||||
);
|
||||
}
|
||||
|
||||
pushIf(
|
||||
errors,
|
||||
!sectionSource.includes(
|
||||
'from "@/lib/agentRuntime/modalityTaskIndexPresentation"',
|
||||
),
|
||||
`${HARNESS_TASK_INDEX_SECTION_PATH} must import the shared taskIndex presentation helpers`,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!sectionSource.includes("任务中心过滤列表"),
|
||||
`${HARNESS_TASK_INDEX_SECTION_PATH} must keep the task center filter surface attached to shared taskIndex rows`,
|
||||
);
|
||||
pushIf(
|
||||
errors,
|
||||
!panelSource.includes(
|
||||
'import { HarnessTaskIndexSection } from "./HarnessTaskIndexSection";',
|
||||
) || !panelSource.includes("<HarnessTaskIndexSection"),
|
||||
`${HARNESS_STATUS_PANEL_PATH} must delegate the taskIndex surface to HarnessTaskIndexSection`,
|
||||
);
|
||||
|
||||
const forbiddenPanelSnippets = [
|
||||
"buildModalityTaskIndexRows",
|
||||
"filterModalityTaskIndexRows",
|
||||
"function TaskIndexItemCard",
|
||||
"function TaskIndexFilterSelect",
|
||||
"function TaskIndexSummarySection",
|
||||
];
|
||||
for (const snippet of forbiddenPanelSnippets) {
|
||||
pushIf(
|
||||
errors,
|
||||
panelSource.includes(snippet),
|
||||
`${HARNESS_STATUS_PANEL_PATH} must not inline taskIndex query/list UI (${snippet}); use ${HARNESS_TASK_INDEX_SECTION_PATH}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { errors };
|
||||
}
|
||||
|
||||
function renderSuccess(
|
||||
registry,
|
||||
matrix,
|
||||
graph,
|
||||
contractReport,
|
||||
profileReport,
|
||||
taskIndexPresentationReport,
|
||||
) {
|
||||
const currentCount = registry.contracts.filter(
|
||||
(contract) => contract.lifecycle === "current",
|
||||
).length;
|
||||
@@ -1200,6 +1454,10 @@ function renderSuccess(registry, matrix, graph, contractReport, profileReport) {
|
||||
` artifact kinds: ${graph.artifact_kinds.length}`,
|
||||
` entry bindings: ${contractReport.entryBindingReport.entryBindingCount}`,
|
||||
` task index core fields: ${REQUIRED_ARTIFACT_INDEX_FIELDS.size}`,
|
||||
` media phase8 index fields: ${REQUIRED_MEDIA_TASK_PHASE8_INDEX_FIELDS.size}`,
|
||||
` task index presentation guard: ${
|
||||
taskIndexPresentationReport.errors.length === 0 ? "current" : "failed"
|
||||
}`,
|
||||
` execution profiles: ${profileReport.profileCount}`,
|
||||
` executor adapters: ${profileReport.adapterCount}`,
|
||||
` registry: ${CONTRACT_PATH}`,
|
||||
@@ -1227,12 +1485,14 @@ function main() {
|
||||
matrixReport,
|
||||
graphReport,
|
||||
);
|
||||
const taskIndexPresentationReport = validateTaskIndexPresentationGuard();
|
||||
const errors = [
|
||||
...validateRequiredDocs(),
|
||||
...matrixReport.errors,
|
||||
...graphReport.errors,
|
||||
...contractReport.errors,
|
||||
...profileReport.errors,
|
||||
...taskIndexPresentationReport.errors,
|
||||
];
|
||||
|
||||
if (errors.length > 0) {
|
||||
@@ -1243,7 +1503,16 @@ function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(renderSuccess(registry, matrix, graph, contractReport, profileReport));
|
||||
console.log(
|
||||
renderSuccess(
|
||||
registry,
|
||||
matrix,
|
||||
graph,
|
||||
contractReport,
|
||||
profileReport,
|
||||
taskIndexPresentationReport,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
#!/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",
|
||||
invokeUrl: "http://127.0.0.1:3030/invoke",
|
||||
timeoutMs: 180_000,
|
||||
intervalMs: 1_000,
|
||||
};
|
||||
|
||||
const ACTION_TIMEOUT_MS = 45_000;
|
||||
const POST_HEALTH_SETTLE_MS = 1_000;
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Lime Design Canvas Smoke
|
||||
|
||||
用途:
|
||||
通过真实 Lime 页面验证 canvas:design Artifact 能进入 LayeredDesignDocument
|
||||
图层设计画布,并能完成基础图层选择与移动交互。
|
||||
|
||||
用法:
|
||||
npm run smoke:design-canvas
|
||||
|
||||
选项:
|
||||
--app-url <url> 前端地址,默认 http://127.0.0.1:1420/
|
||||
--health-url <url> DevBridge 健康检查地址,默认 http://127.0.0.1:3030/health
|
||||
--invoke-url <url> DevBridge invoke 地址,默认 http://127.0.0.1:3030/invoke
|
||||
--timeout-ms <ms> 总超时,默认 180000
|
||||
--interval-ms <ms> 轮询间隔,默认 1000
|
||||
-h, --help 显示帮助
|
||||
`);
|
||||
}
|
||||
|
||||
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 === "--invoke-url" && argv[index + 1]) {
|
||||
options.invokeUrl = 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 === "--help" || arg === "-h") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 30_000) {
|
||||
throw new Error("--timeout-ms 必须是 >= 30000 的数字");
|
||||
}
|
||||
if (!Number.isFinite(options.intervalMs) || options.intervalMs < 100) {
|
||||
throw new Error("--interval-ms 必须是 >= 100 的数字");
|
||||
}
|
||||
if (!options.appUrl || !options.healthUrl || !options.invokeUrl) {
|
||||
throw new Error("--app-url、--health-url、--invoke-url 均不能为空");
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function logStage(label) {
|
||||
console.log(`[smoke:design-canvas] stage=${label}`);
|
||||
}
|
||||
|
||||
function pickStringField(target, ...keys) {
|
||||
if (!target || typeof target !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = target[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function waitForHealth(options) {
|
||||
const startedAt = Date.now();
|
||||
let lastError = null;
|
||||
|
||||
while (Date.now() - startedAt < options.timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(options.healthUrl, { method: "GET" });
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
console.log(
|
||||
`[smoke:design-canvas] DevBridge 已就绪 (${Date.now() - startedAt}ms)${
|
||||
payload?.status ? ` status=${payload.status}` : ""
|
||||
}`,
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await sleep(options.intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
const detail =
|
||||
lastError instanceof Error
|
||||
? lastError.message
|
||||
: String(lastError || "unknown error");
|
||||
throw new Error(
|
||||
`[smoke:design-canvas] DevBridge 未就绪,请先启动 npm run tauri:dev:headless。最后错误: ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function invoke(options, cmd, args) {
|
||||
const response = await fetch(options.invokeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ cmd, args }),
|
||||
signal: AbortSignal.timeout(Math.min(options.timeoutMs, 180_000)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload?.error) {
|
||||
throw new Error(String(payload.error));
|
||||
}
|
||||
|
||||
return payload?.result;
|
||||
}
|
||||
|
||||
async function resolveDefaultWorkspace(options) {
|
||||
const defaultProject = await invoke(options, "get_or_create_default_project");
|
||||
assert(
|
||||
defaultProject && typeof defaultProject === "object",
|
||||
"get_or_create_default_project 返回为空",
|
||||
);
|
||||
|
||||
const projectId = pickStringField(defaultProject, "id");
|
||||
assert(projectId, "默认 workspace 缺少 id");
|
||||
|
||||
const ensuredWorkspace = await invoke(options, "workspace_ensure_ready", {
|
||||
id: projectId,
|
||||
});
|
||||
const rootPath =
|
||||
pickStringField(ensuredWorkspace, "rootPath", "root_path") ||
|
||||
pickStringField(defaultProject, "rootPath", "root_path");
|
||||
assert(rootPath, "默认 workspace 缺少 rootPath");
|
||||
|
||||
return {
|
||||
projectId,
|
||||
rootPath,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSmokeUrl(options, workspace) {
|
||||
const url = new URL("/design-canvas-smoke", options.appUrl);
|
||||
url.searchParams.set("projectRootPath", workspace.rootPath);
|
||||
url.searchParams.set("projectId", workspace.projectId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function waitForText(page, label, text) {
|
||||
try {
|
||||
await page.getByText(text).first().waitFor({
|
||||
state: "visible",
|
||||
timeout: ACTION_TIMEOUT_MS,
|
||||
});
|
||||
} catch (error) {
|
||||
const bodyText = await page.locator("body").innerText().catch(() => "");
|
||||
throw new Error(
|
||||
`[smoke:design-canvas] ${label} 等待失败,缺少文本 ${JSON.stringify(
|
||||
text,
|
||||
)};页面文本片段: ${JSON.stringify(bodyText.slice(0, 1200))}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPageFlow(options, smokeUrl) {
|
||||
const userDataDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `lime-design-canvas-smoke-${process.pid}-`),
|
||||
);
|
||||
const launchOptions = {
|
||||
headless: true,
|
||||
viewport: { width: 1440, height: 980 },
|
||||
};
|
||||
let context = null;
|
||||
|
||||
try {
|
||||
context = await chromium.launchPersistentContext(userDataDir, {
|
||||
...launchOptions,
|
||||
channel: "chrome",
|
||||
});
|
||||
} catch (chromeError) {
|
||||
console.warn(
|
||||
`[smoke:design-canvas] Chrome channel 启动失败,尝试 Playwright 自带 Chromium: ${
|
||||
chromeError instanceof Error ? chromeError.message : String(chromeError)
|
||||
}`,
|
||||
);
|
||||
context = await chromium.launchPersistentContext(userDataDir, launchOptions);
|
||||
}
|
||||
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
const consoleErrors = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
consoleErrors.push(error.stack || error.message);
|
||||
});
|
||||
|
||||
try {
|
||||
logStage("open-design-canvas-page");
|
||||
await page.goto(smokeUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
|
||||
logStage("wait-design-canvas");
|
||||
await page
|
||||
.locator('[data-testid="design-canvas-smoke-page"]')
|
||||
.waitFor({ state: "visible", timeout: ACTION_TIMEOUT_MS });
|
||||
await page
|
||||
.locator('[data-testid="design-canvas"]')
|
||||
.waitFor({ state: "visible", timeout: ACTION_TIMEOUT_MS });
|
||||
|
||||
await waitForText(page, "smoke 标题", "canvas:design 专属 GUI Smoke");
|
||||
await waitForText(page, "artifact 类型", "canvas:design");
|
||||
await waitForText(page, "事实源标记", "LayeredDesignDocument");
|
||||
await waitForText(page, "画布标题", "Smoke 图层设计海报");
|
||||
await waitForText(page, "图层栏", "图层");
|
||||
await waitForText(page, "属性栏", "属性");
|
||||
await waitForText(page, "生成入口", "生成全部图片层");
|
||||
await waitForText(page, "刷新入口", "刷新生成结果");
|
||||
await waitForText(page, "单层重生成入口", "重生成当前层");
|
||||
await waitForText(page, "导出入口", "导出设计工程");
|
||||
|
||||
logStage("interact-layer");
|
||||
await page.getByRole("button", { name: "选择图层 主标题" }).click({
|
||||
timeout: ACTION_TIMEOUT_MS,
|
||||
});
|
||||
await waitForText(page, "主标题选中", "主标题");
|
||||
await page.getByRole("button", { name: "右移", exact: true }).click({
|
||||
timeout: ACTION_TIMEOUT_MS,
|
||||
});
|
||||
await page.getByRole("button", { name: "隐藏", exact: true }).click({
|
||||
timeout: ACTION_TIMEOUT_MS,
|
||||
});
|
||||
await page.getByRole("button", { name: "显示", exact: true }).click({
|
||||
timeout: ACTION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
throw new Error(
|
||||
`[smoke:design-canvas] 页面存在 ${consoleErrors.length} 条 console error: ${JSON.stringify(
|
||||
consoleErrors.slice(0, 5),
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await context.close().catch(() => undefined);
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("当前 Node 运行时不支持 fetch,请使用 Node 18+");
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
|
||||
logStage("wait-health");
|
||||
await waitForHealth(options);
|
||||
await sleep(POST_HEALTH_SETTLE_MS);
|
||||
|
||||
logStage("resolve-default-workspace");
|
||||
const workspace = await resolveDefaultWorkspace(options);
|
||||
const smokeUrl = buildSmokeUrl(options, workspace);
|
||||
|
||||
await runPageFlow(options, smokeUrl);
|
||||
|
||||
console.log(
|
||||
`[smoke:design-canvas] 通过 project=${workspace.projectId} root=${workspace.rootPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : String(error || "unknown error"),
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -49,6 +49,18 @@ const SECONDARY_PACK = {
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
const AGENT_RESULT_MESSAGE = {
|
||||
id: "smoke-agent-result-knowledge",
|
||||
title: "对话结果资料",
|
||||
content: [
|
||||
"# 对话结果资料",
|
||||
"",
|
||||
"- 事实:该结果来自当前 Agent 对话,用于验证生成结果可以沉淀成项目资料。",
|
||||
"- 适用场景:用户拿到一段可复用结论后,可以一键保存,随后在项目资料管理页检查确认。",
|
||||
"- 风险提示:沉淀后仍需人工确认,避免把临时分析当成长期事实。",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
Lime Knowledge GUI Smoke
|
||||
@@ -299,6 +311,34 @@ async function clickPageControl(page, { text, ariaLabel, index = 0 }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function seedAgentResultForKnowledgeCapture(page, options) {
|
||||
await page.evaluate(
|
||||
({ projectId, message }) => {
|
||||
const now = new Date().toISOString();
|
||||
sessionStorage.setItem(
|
||||
`aster_messages_${projectId}`,
|
||||
JSON.stringify([
|
||||
{
|
||||
id: message.id,
|
||||
role: "assistant",
|
||||
content: message.content,
|
||||
timestamp: now,
|
||||
},
|
||||
]),
|
||||
);
|
||||
sessionStorage.removeItem(`aster_curr_sessionId_${projectId}`);
|
||||
sessionStorage.removeItem(`aster_last_sessionId_${projectId}`);
|
||||
sessionStorage.removeItem(`aster_thread_turns_${projectId}`);
|
||||
sessionStorage.removeItem(`aster_thread_items_${projectId}`);
|
||||
sessionStorage.removeItem(`aster_curr_turnId_${projectId}`);
|
||||
},
|
||||
{
|
||||
projectId: options.projectId,
|
||||
message: AGENT_RESULT_MESSAGE,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function createSmokeProject(options) {
|
||||
const projectName = `Knowledge GUI Smoke ${process.pid}`;
|
||||
const project = await invoke(options, "workspace_create", {
|
||||
@@ -314,6 +354,13 @@ async function createSmokeProject(options) {
|
||||
}
|
||||
options.projectId = projectId;
|
||||
options.projectName = String(project?.name || projectName);
|
||||
const projectRootPath = String(
|
||||
project?.rootPath || project?.root_path || "",
|
||||
).trim();
|
||||
if (projectRootPath) {
|
||||
options.workingDir = projectRootPath;
|
||||
fs.mkdirSync(options.workingDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupSmokeProject(options) {
|
||||
@@ -394,14 +441,14 @@ async function runPlaywrightGuiFlow(options) {
|
||||
await waitForPageText(page, "首页加载", ["青柠一下,灵感即来"], options.timeoutMs);
|
||||
|
||||
logStage("open-knowledge-page");
|
||||
await clickPageControl(page, { ariaLabel: "知识库" });
|
||||
await clickPageControl(page, { ariaLabel: "项目资料" });
|
||||
|
||||
logStage("wait-knowledge-overview");
|
||||
await waitForPageText(
|
||||
page,
|
||||
"知识库总览加载",
|
||||
[
|
||||
"项目资料管理",
|
||||
"项目资料",
|
||||
"日常使用入口",
|
||||
"回到 Agent",
|
||||
"全部资料",
|
||||
@@ -424,7 +471,10 @@ async function runPlaywrightGuiFlow(options) {
|
||||
await waitForPageText(
|
||||
page,
|
||||
"Agent 页面加载",
|
||||
["项目资料:未使用", "请基于当前项目资料生成内容"],
|
||||
[
|
||||
`正在使用:${DEFAULT_PACK.title}`,
|
||||
"请基于当前项目资料生成内容",
|
||||
],
|
||||
options.timeoutMs,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -438,8 +488,51 @@ async function runPlaywrightGuiFlow(options) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logStage("return-knowledge-before-agent-result");
|
||||
await clickPageControl(page, { ariaLabel: "项目资料" });
|
||||
|
||||
logStage("prepare-agent-result");
|
||||
await seedAgentResultForKnowledgeCapture(page, options);
|
||||
await clickPageControl(page, { text: "用于生成" });
|
||||
|
||||
logStage("wait-agent-result");
|
||||
await waitForPageText(
|
||||
page,
|
||||
"Agent 结果样本加载",
|
||||
[
|
||||
`正在使用:${DEFAULT_PACK.title}`,
|
||||
"沉淀为项目资料",
|
||||
"事实:该结果来自当前 Agent 对话",
|
||||
],
|
||||
options.timeoutMs,
|
||||
);
|
||||
|
||||
logStage("capture-agent-result");
|
||||
await clickPageControl(page, { ariaLabel: "沉淀为项目资料" });
|
||||
|
||||
logStage("wait-agent-result-captured");
|
||||
await waitForPageText(
|
||||
page,
|
||||
"Agent 结果沉淀完成",
|
||||
["项目资料已整理", AGENT_RESULT_MESSAGE.title],
|
||||
options.timeoutMs,
|
||||
);
|
||||
|
||||
logStage("return-knowledge-page");
|
||||
await clickPageControl(page, { ariaLabel: "知识库" });
|
||||
await clickPageControl(page, { ariaLabel: "项目资料" });
|
||||
|
||||
logStage("wait-captured-agent-result");
|
||||
await waitForPageText(
|
||||
page,
|
||||
"沉淀资料进入管理页",
|
||||
[
|
||||
"项目资料",
|
||||
"全部资料",
|
||||
AGENT_RESULT_MESSAGE.title,
|
||||
"继续确认",
|
||||
],
|
||||
options.timeoutMs,
|
||||
);
|
||||
|
||||
logStage("open-import-view");
|
||||
await clickPageControl(page, { text: "补充导入" });
|
||||
@@ -518,12 +611,12 @@ async function main() {
|
||||
await waitForHealth(options);
|
||||
await sleep(POST_HEALTH_SETTLE_MS);
|
||||
|
||||
logStage("seed-knowledge-packs");
|
||||
await seedKnowledgePacks(options);
|
||||
|
||||
logStage("create-smoke-project");
|
||||
await createSmokeProject(options);
|
||||
|
||||
logStage("seed-knowledge-packs");
|
||||
await seedKnowledgePacks(options);
|
||||
|
||||
await runPlaywrightGuiFlow(options);
|
||||
console.log("[smoke:knowledge-gui] 通过");
|
||||
} finally {
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("GitHub release asset staging", () => {
|
||||
"arm-sig",
|
||||
);
|
||||
writeFile(
|
||||
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.27.0_aarch64.dmg"),
|
||||
path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.28.0_aarch64.dmg"),
|
||||
);
|
||||
writeFile(path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"));
|
||||
writeFile(
|
||||
@@ -311,7 +311,7 @@ describe("GitHub release asset staging", () => {
|
||||
"x64-sig",
|
||||
);
|
||||
writeFile(
|
||||
path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.27.0_x64.dmg"),
|
||||
path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.28.0_x64.dmg"),
|
||||
);
|
||||
writeFile(latestPath, "{}");
|
||||
|
||||
@@ -319,29 +319,29 @@ describe("GitHub release asset staging", () => {
|
||||
assetsDir,
|
||||
extraAssets: [latestPath],
|
||||
outDir,
|
||||
version: "v1.27.0",
|
||||
version: "v1.28.0",
|
||||
});
|
||||
|
||||
expect(copied.map((item) => item.name).sort()).toEqual(
|
||||
[
|
||||
"Lime_1.27.0_aarch64.app.tar.gz",
|
||||
"Lime_1.27.0_aarch64.app.tar.gz.sig",
|
||||
"Lime_1.27.0_aarch64.dmg",
|
||||
"Lime_1.27.0_x64.app.tar.gz",
|
||||
"Lime_1.27.0_x64.app.tar.gz.sig",
|
||||
"Lime_1.27.0_x64.dmg",
|
||||
"Lime_1.28.0_aarch64.app.tar.gz",
|
||||
"Lime_1.28.0_aarch64.app.tar.gz.sig",
|
||||
"Lime_1.28.0_aarch64.dmg",
|
||||
"Lime_1.28.0_x64.app.tar.gz",
|
||||
"Lime_1.28.0_x64.app.tar.gz.sig",
|
||||
"Lime_1.28.0_x64.dmg",
|
||||
"latest.json",
|
||||
].sort(),
|
||||
);
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(outDir, "Lime_1.27.0_aarch64.app.tar.gz.sig"),
|
||||
path.join(outDir, "Lime_1.28.0_aarch64.app.tar.gz.sig"),
|
||||
"utf8",
|
||||
),
|
||||
).toBe("arm-sig");
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(outDir, "Lime_1.27.0_x64.app.tar.gz.sig"),
|
||||
path.join(outDir, "Lime_1.28.0_x64.app.tar.gz.sig"),
|
||||
"utf8",
|
||||
),
|
||||
).toBe("x64-sig");
|
||||
|
||||
@@ -1424,6 +1424,27 @@ async function main() {
|
||||
options.timeoutMs + 30_000,
|
||||
);
|
||||
|
||||
runCommand(
|
||||
npmCommand,
|
||||
[
|
||||
"run",
|
||||
"smoke:design-canvas",
|
||||
"--",
|
||||
"--app-url",
|
||||
options.appUrl,
|
||||
"--health-url",
|
||||
options.healthUrl,
|
||||
"--invoke-url",
|
||||
options.invokeUrl,
|
||||
"--timeout-ms",
|
||||
String(options.timeoutMs),
|
||||
"--interval-ms",
|
||||
String(options.intervalMs),
|
||||
],
|
||||
"smoke:design-canvas",
|
||||
options.timeoutMs + 30_000,
|
||||
);
|
||||
|
||||
await cleanupStaleGuiSmokeChromeProfiles(options, {
|
||||
label: "收尾清理本轮 smoke Chrome profiles",
|
||||
required: true,
|
||||
|
||||
Reference in New Issue
Block a user