Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ export default function App() {
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [invocation, setInvocation] = useState<FrontendInvocation>(emptyInvocation);
const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null);
const [agentInfoRefreshKey, setAgentInfoRefreshKey] = useState(0);
const [capabilitiesLoading, setCapabilitiesLoading] = useState(false);
const [sessionCapabilities, setSessionCapabilities] =
useState<SessionCapabilities | null>(null);
Expand Down Expand Up @@ -1262,6 +1263,7 @@ export default function App() {
result.version,
);
setConnections(loadConnections());
setAgentInfoRefreshKey((key) => key + 1);
setLibraryRuntimeIds((current) => {
const next = new Set(current ?? []);
next.add(result.runtimeId!);
Expand Down Expand Up @@ -1692,7 +1694,7 @@ export default function App() {
return () => {
cancelled = true;
};
}, [appName]);
}, [appName, agentInfoRefreshKey]);
useEffect(() => {
if (!access) return;
localStorage.setItem(
Expand Down Expand Up @@ -2679,6 +2681,7 @@ export default function App() {
// background stream keeps persisting to its own (old) session.
const selectAgent = (id: string) => {
setConnections(loadConnections());
if (id === appName) setAgentInfoRefreshKey((key) => key + 1);
viewSidRef.current = "";
setSessionId("");
setMyAgents(false);
Expand Down Expand Up @@ -2708,6 +2711,7 @@ export default function App() {
agent.runtime.currentVersion,
);
setConnections(loadConnections());
setAgentInfoRefreshKey((key) => key + 1);
setAgentDetailTarget(null);
setMyAgents(false);
setManageAgents(false);
Expand Down
76 changes: 73 additions & 3 deletions frontend/src/create/CustomCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,50 @@ import "./CustomCreate.css";

const MarkdownPromptEditor = lazy(() => import("./MarkdownPromptEditor"));

const DEBUG_TEST_RUN_STORAGE_KEY = "veadk.generatedAgentTestRuns";

function readStoredDebugTestRunIds(): string[] {
if (typeof window === "undefined") return [];
try {
const parsed = JSON.parse(
window.sessionStorage.getItem(DEBUG_TEST_RUN_STORAGE_KEY) ?? "[]",
);
if (!Array.isArray(parsed)) return [];
return parsed.filter(
(item): item is string => typeof item === "string" && item.length > 0,
);
} catch {
return [];
}
}

function writeStoredDebugTestRunIds(runIds: string[]) {
if (typeof window === "undefined") return;
const uniqueRunIds = Array.from(new Set(runIds)).slice(-20);
try {
if (uniqueRunIds.length) {
window.sessionStorage.setItem(
DEBUG_TEST_RUN_STORAGE_KEY,
JSON.stringify(uniqueRunIds),
);
} else {
window.sessionStorage.removeItem(DEBUG_TEST_RUN_STORAGE_KEY);
}
} catch {
// Best-effort cleanup bookkeeping only.
}
}

function rememberDebugTestRun(runId: string) {
writeStoredDebugTestRunIds([...readStoredDebugTestRunIds(), runId]);
}

function forgetDebugTestRun(runId: string) {
writeStoredDebugTestRunIds(
readStoredDebugTestRunIds().filter((item) => item !== runId),
);
}

/** Trigger a browser download of a text file. */
function downloadText(filename: string, text: string, mime = "text/plain") {
const url = URL.createObjectURL(
Expand Down Expand Up @@ -2341,12 +2385,33 @@ export function CustomCreate({
const scrollRef = useRef<HTMLDivElement | null>(null);
const sectionRefs = useRef<Partial<Record<StepId, HTMLElement | null>>>({});

async function cleanupStoredDebugRuns() {
const activeRunIds = new Set(
[...debugRunsRef.current.values()].map(({ run }) => run.runId),
);
const staleRunIds = readStoredDebugTestRunIds().filter(
(runId) => !activeRunIds.has(runId),
);
if (!staleRunIds.length) return;
await Promise.all(
staleRunIds.map(async (runId) => {
try {
await deleteGeneratedAgentTestRun(runId);
forgetDebugTestRun(runId);
} catch (err) {
console.warn("清理遗留调试运行失败", err);
}
}),
);
}

useEffect(() => {
void cleanupStoredDebugRuns();
return () => {
for (const { run } of debugRunsRef.current.values()) {
deleteGeneratedAgentTestRun(run.runId).catch((err) =>
console.warn("清理调试运行失败", err),
);
deleteGeneratedAgentTestRun(run.runId)
.then(() => forgetDebugTestRun(run.runId))
.catch((err) => console.warn("清理调试运行失败", err));
}
debugRunsRef.current.clear();
};
Expand Down Expand Up @@ -2727,6 +2792,7 @@ export function CustomCreate({
runs.map(async ({ run }) => {
try {
await deleteGeneratedAgentTestRun(run.runId);
forgetDebugTestRun(run.runId);
} catch (err) {
console.warn("清理调试运行失败", err);
}
Expand All @@ -2741,6 +2807,7 @@ export function CustomCreate({
setActiveDebugRunCount(debugRunsRef.current.size);
try {
await deleteGeneratedAgentTestRun(runtime.run.runId);
forgetDebugTestRun(runtime.run.runId);
} catch (err) {
console.warn("清理调试运行失败", err);
}
Expand Down Expand Up @@ -2829,6 +2896,7 @@ export function CustomCreate({
let createdRun: GeneratedAgentTestRun | null = null;
try {
await cleanupDebugVariantRun(id);
await cleanupStoredDebugRuns();
const variantDraft: AgentDraft = {
...draft,
modelName: variant.modelName || draft.modelName,
Expand All @@ -2838,6 +2906,7 @@ export function CustomCreate({
createdRun = await createGeneratedAgentTestRun(
debugRuntimeDraft(variantDraft),
);
rememberDebugTestRun(createdRun.runId);
const sessionId = await createGeneratedAgentTestSession(
createdRun.runId,
"test_user",
Expand All @@ -2855,6 +2924,7 @@ export function CustomCreate({
if (createdRun) {
try {
await deleteGeneratedAgentTestRun(createdRun.runId);
forgetDebugTestRun(createdRun.runId);
} catch (cleanupError) {
console.warn("清理调试运行失败", cleanupError);
}
Expand Down
49 changes: 39 additions & 10 deletions frontend/src/ui/AgentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,23 @@ function feedbackSetFor(
return sets.find((set) => set.kind === kind);
}

function canvasDraftKey(draft: AgentDraft): string {
const visit = (node: AgentDraft): unknown => [
node.name,
node.description,
node.agentType ?? "llm",
node.modelName ?? "",
node.tools ?? [],
node.builtinTools ?? [],
(node.customTools ?? []).map((tool) => tool.name),
(node.mcpTools ?? []).map((tool) => tool.name),
node.skills ?? [],
(node.selectedSkills ?? []).map((skill) => skill.name),
(node.subAgents ?? []).map(visit),
];
return JSON.stringify(visit(draft));
}

const DEPLOYMENT_STEPS = [
{ phase: "prepare", label: "准备部署", description: "校验配置并创建部署任务" },
{ phase: "build", label: "构建镜像", description: "生成运行环境与智能体代码" },
Expand Down Expand Up @@ -652,9 +669,14 @@ export function AgentWorkspace({
)
.sort((left, right) => right.startedAt - left.startedAt)[0];
}, [deploymentTasks, selectedAgent, selectedDraft, selectedPendingTask]);
const draftFlowKey = useMemo(() => canvasDraftKey(draft), [draft]);
const displayCurrentVersion =
selectedAgent?.currentVersion ?? runtimeDetail?.currentVersion ?? null;
const runtimeVersionKey =
displayCurrentVersion ?? selectedPendingTask?.startedAt ?? "unknown";
const executionFlowKey = selectedAgentInfo
? `runtime:${selectedAgent?.runtimeId ?? selectedAgentInfo.name}:${countDraftNodes(draft)}`
: `draft:${selectedPendingTask?.id ?? selectedDraft?.id ?? selectedAgent?.id ?? selectedName}`;
? `runtime:${selectedAgent?.runtimeId ?? selectedAgentInfo.name}:v${runtimeVersionKey}:${draftFlowKey}`
: `draft:${selectedPendingTask?.id ?? selectedDraft?.id ?? selectedAgent?.id ?? selectedName}:${draftFlowKey}`;
const loadingExecutionFlow = Boolean(
detailOnly && selectedAgent?.runtimeId && !detailAgentInfoResolved,
);
Expand Down Expand Up @@ -719,7 +741,12 @@ export function AgentWorkspace({
return () => {
cancelled = true;
};
}, [detailOnly, selectedAgent?.region, selectedAgent?.runtimeId]);
}, [
detailOnly,
selectedAgent?.currentVersion,
selectedAgent?.region,
selectedAgent?.runtimeId,
]);

useEffect(() => {
let cancelled = false;
Expand All @@ -738,7 +765,11 @@ export function AgentWorkspace({
return () => {
cancelled = true;
};
}, [selectedAgent?.region, selectedAgent?.runtimeId]);
}, [
selectedAgent?.currentVersion,
selectedAgent?.region,
selectedAgent?.runtimeId,
]);

useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -1502,8 +1533,8 @@ export function AgentWorkspace({
<div>
<div className="aw-agent-title-row">
<h2>{selectedName}</h2>
{selectedAgent?.currentVersion != null && (
<span>v{selectedAgent.currentVersion}</span>
{displayCurrentVersion != null && (
<span>v{displayCurrentVersion}</span>
)}
{selectedDraft && <span>草稿</span>}
{selectedAgentUpdateDraft && <span>待更新</span>}
Expand Down Expand Up @@ -1647,10 +1678,8 @@ export function AgentWorkspace({
<div>
<dt>当前版本</dt>
<dd>
{runtimeDetail?.currentVersion != null
? `v${runtimeDetail.currentVersion}`
: selectedAgent?.currentVersion != null
? `v${selectedAgent.currentVersion}`
{displayCurrentVersion != null
? `v${displayCurrentVersion}`
: "暂未提供"}
</dd>
</div>
Expand Down
19 changes: 19 additions & 0 deletions frontend/tests/agentWorkspace.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,24 @@ test("agent details show capability badges and deployment state before the flow"
assert.match(workspaceSource, /status\.toLowerCase\(\) === "ready"[\s\S]*?className="aw-status-dot"/);
assert.match(workspaceStyles, /\.aw-readonly-config dd\.is-ready\s*\{[\s\S]*?color:\s*hsl\(142 62% 30%\)/);
assert.match(workspaceSource, /const executionFlowKey = selectedAgentInfo/);
assert.match(workspaceSource, /const draftFlowKey = useMemo\(\(\) => canvasDraftKey\(draft\), \[draft\]\)/);
assert.match(workspaceSource, /const displayCurrentVersion =[\s\S]*?selectedAgent\?\.currentVersion \?\? runtimeDetail\?\.currentVersion \?\? null/);
assert.match(workspaceSource, /const runtimeVersionKey =[\s\S]*?displayCurrentVersion \?\? selectedPendingTask\?\.startedAt/);
assert.match(workspaceSource, /<span>v\{displayCurrentVersion\}<\/span>/);
assert.match(workspaceSource, /\? `v\$\{displayCurrentVersion\}`[\s\S]*?: "暂未提供"/);
assert.match(workspaceSource, /detailOnly && selectedAgent\?\.runtimeId && !detailAgentInfoResolved/);
assert.match(
workspaceSource,
/loadingExecutionFlow \? \([\s\S]*?className="aw-canvas-loading"[\s\S]*?正在加载执行流程[\s\S]*?<AgentBuildCanvas[\s\S]*?key=\{executionFlowKey\}/,
);
assert.match(
workspaceSource,
/`runtime:\$\{selectedAgent\?\.runtimeId \?\? selectedAgentInfo\.name\}:v\$\{runtimeVersionKey\}:\$\{draftFlowKey\}`/,
);
assert.match(
workspaceSource,
/selectedAgent\?\.currentVersion,[\s\S]*?selectedAgent\?\.region,[\s\S]*?selectedAgent\?\.runtimeId/,
);
assert.match(workspaceStyles, /\.aw-canvas-loading\s*\{[\s\S]*?align-items:\s*center;/);
});

Expand All @@ -109,6 +122,12 @@ test("workspace publish flow restores PR 748 deployment lifecycle hooks", () =>
assert.match(appSource, /setAgentDetailTarget\(null\)[\s\S]*?setFocusedDeploymentTaskId\(task\.id\)/);
assert.match(appSource, /const finishDeployment = useCallback/);
assert.match(appSource, /await connectRuntime\([\s\S]*?result\.runtimeId[\s\S]*?result\.version/);
assert.match(appSource, /const \[agentInfoRefreshKey, setAgentInfoRefreshKey\] = useState\(0\)/);
assert.match(appSource, /}, \[appName, agentInfoRefreshKey\]\);/);
assert.match(
appSource,
/const finishDeployment = useCallback[\s\S]*?setConnections\(loadConnections\(\)\);[\s\S]*?setAgentInfoRefreshKey\(\(key\) => key \+ 1\)/,
);
assert.match(appSource, /onDeploymentStarted=\{openDeploymentDetail\}/);
assert.match(appSource, /onDeploymentComplete=\{finishDeployment\}/);

Expand Down
19 changes: 19 additions & 0 deletions frontend/tests/debugErrorPresentation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,22 @@ test("debug errors support full expansion, copying, and footer restart", () => {
/className="cw-ab-start cw-ab-footer-start"[\s\S]*?onClick=\{\(\) => onStartVariant\(variant\.id\)\}/,
);
});

test("debug test runs are persisted and reclaimed after refresh", () => {
assert.match(source, /const DEBUG_TEST_RUN_STORAGE_KEY = "veadk\.generatedAgentTestRuns"/);
assert.match(source, /window\.sessionStorage\.getItem\(DEBUG_TEST_RUN_STORAGE_KEY\)/);
assert.match(source, /window\.sessionStorage\.setItem\(/);
assert.match(source, /function rememberDebugTestRun\(runId: string\)/);
assert.match(source, /function forgetDebugTestRun\(runId: string\)/);
assert.match(source, /async function cleanupStoredDebugRuns\(\)/);
assert.match(
source,
/const activeRunIds = new Set\([\s\S]*?debugRunsRef\.current\.values\(\)[\s\S]*?run\.runId/,
);
assert.match(
source,
/await cleanupStoredDebugRuns\(\);[\s\S]*?createdRun = await createGeneratedAgentTestRun/,
);
assert.match(source, /rememberDebugTestRun\(createdRun\.runId\)/);
assert.match(source, /forgetDebugTestRun\(runtime\.run\.runId\)/);
});
11 changes: 11 additions & 0 deletions tests/cli/test_generated_agent_backend_codegen_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,17 @@ def test_frontend_deploy_forwards_a2a_registry_runtime_env_keys() -> None:
assert '"A2A_REGISTRY_ACCESS_KEY",' in source


def test_generated_agent_test_run_limit_is_owner_scoped() -> None:
source = Path("veadk/cli/cli_frontend.py").read_text()

assert "_test_runs_creating: dict[str, int]" in source
assert 'owner_id = principal.owner_id if principal else ""' in source
assert "active_count = sum(" in source
assert "1 for run in _test_runs.values() if run.owner_id == owner_id" in source
assert "_test_runs_creating.get(owner_id, 0)" in source
assert "owner_id=owner_id" in source


def test_generated_agent_test_runner_enables_dynamic_a2a_helper() -> None:
source = Path("veadk/cli/generated_agent_test_runner.py").read_text()

Expand Down
Loading
Loading