Bound design system audit auto repair

This commit is contained in:
qiongyu1999
2026-05-19 01:26:25 +08:00
parent 534bc50948
commit e1c58c8eb5
2 changed files with 102 additions and 8 deletions
+29 -4
View File
@@ -190,6 +190,7 @@ const MAX_CHAT_PANEL_WIDTH = 720;
const MIN_WORKSPACE_PANEL_WIDTH = 400;
const SPLIT_RESIZE_HANDLE_WIDTH = 8;
const CHAT_PANEL_KEYBOARD_STEP = 16;
const DESIGN_SYSTEM_AUDIT_AUTO_REPAIR_ATTEMPTS = 2;
const MIN_NORMAL_SPLIT_WIDTH =
MIN_CHAT_PANEL_WIDTH + SPLIT_RESIZE_HANDLE_WIDTH + MIN_WORKSPACE_PANEL_WIDTH;
type DesignSystemReviewEntry = NonNullable<ProjectMetadata['designSystemReview']>[string];
@@ -320,7 +321,10 @@ function clearAutoSendSession(projectId: string): void {
function markDesignSystemAuditAutoRepairEligible(projectId: string): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.setItem(designSystemAuditAutoRepairKey(projectId), '1');
window.sessionStorage.setItem(
designSystemAuditAutoRepairKey(projectId),
String(DESIGN_SYSTEM_AUDIT_AUTO_REPAIR_ATTEMPTS),
);
} catch {
/* ignore */
}
@@ -330,14 +334,33 @@ function consumeDesignSystemAuditAutoRepair(projectId: string): boolean {
if (typeof window === 'undefined') return false;
try {
const key = designSystemAuditAutoRepairKey(projectId);
const eligible = Boolean(window.sessionStorage.getItem(key));
window.sessionStorage.removeItem(key);
return eligible;
const raw = window.sessionStorage.getItem(key);
const attemptsRemaining = raw ? Number.parseInt(raw, 10) : 0;
if (!Number.isFinite(attemptsRemaining) || attemptsRemaining <= 0) {
window.sessionStorage.removeItem(key);
return false;
}
const nextAttemptsRemaining = attemptsRemaining - 1;
if (nextAttemptsRemaining > 0) {
window.sessionStorage.setItem(key, String(nextAttemptsRemaining));
} else {
window.sessionStorage.removeItem(key);
}
return true;
} catch {
return false;
}
}
function clearDesignSystemAuditAutoRepair(projectId: string): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.removeItem(designSystemAuditAutoRepairKey(projectId));
} catch {
/* ignore */
}
}
function isDesignSystemWorkspaceMetadata(metadata: ProjectMetadata | undefined): boolean {
return metadata?.importedFrom === 'design-system';
}
@@ -1324,6 +1347,8 @@ export function ProjectView({
if (consumeDesignSystemAuditAutoRepair(project.id)) {
setAutoAuditRepairSeed(seed);
}
} else {
clearDesignSystemAuditAutoRepair(project.id);
}
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
@@ -407,7 +407,7 @@ describe('ProjectView daemon cleanup', () => {
}
});
it('audits design-system workspace output after first auto-send and auto-sends one repair prompt', async () => {
it('audits design-system workspace output after first auto-send and auto-sends bounded repair prompts', async () => {
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
listMessages.mockResolvedValue([]);
fetchPreviewComments.mockResolvedValue([]);
@@ -476,13 +476,14 @@ describe('ProjectView daemon cleanup', () => {
);
await waitFor(() => expect(fetchProjectDesignSystemPackageAudit).toHaveBeenCalledWith('project-ds'));
await waitFor(() => expect(streamViaDaemon).toHaveBeenCalledTimes(2));
expect(saveMessage.mock.calls.some((call) =>
await waitFor(() => expect(streamViaDaemon).toHaveBeenCalledTimes(3));
const repairMessages = saveMessage.mock.calls.filter((call) =>
call[2]?.role === 'user'
&& typeof call[2]?.content === 'string'
&& call[2].content.includes('Fix the design-system package audit findings below.')
&& call[2].content.includes('ui_kit_index_missing_runtime_bootstrap'),
)).toBe(true);
);
expect(repairMessages).toHaveLength(2);
expect(window.sessionStorage.getItem('od:design-system-audit-auto-repair:project-ds')).toBeNull();
await waitFor(() => {
const repairSeed = chatPaneSpy.mock.calls.find(
@@ -502,6 +503,74 @@ describe('ProjectView daemon cleanup', () => {
)).toBe(true);
});
it('clears design-system auto-repair budget when the first audit passes', async () => {
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
listMessages.mockResolvedValue([]);
fetchPreviewComments.mockResolvedValue([]);
loadTabs.mockResolvedValue({ tabs: [], activeTabId: null });
fetchProjectFiles.mockResolvedValue([]);
fetchLiveArtifacts.mockResolvedValue([]);
fetchSkill.mockResolvedValue(null);
fetchDesignSystem.mockResolvedValue(null);
getTemplate.mockResolvedValue(null);
listActiveChatRuns.mockResolvedValue([]);
fetchProjectDesignSystemPackageAudit.mockResolvedValue({
ok: true,
projectPath: '/tmp/ds',
filesInspected: 24,
errors: [],
warnings: [],
});
streamViaDaemon.mockImplementation(async (options: {
handlers: { onDone: () => void };
onRunCreated?: (runId: string) => void;
}) => {
options.onRunCreated?.('run-ds-pass');
options.handlers.onDone();
});
chatPaneSpy.mockClear();
window.sessionStorage.setItem('od:auto-send-first:project-ds-pass', '1');
render(
<ProjectView
project={{
id: 'project-ds-pass',
name: 'Passing Design System',
skillId: null,
designSystemId: 'user:passing-ds',
pendingPrompt: 'Create this project as a design system.',
metadata: {
importedFrom: 'design-system',
entryFile: 'DESIGN.md',
sourceFileName: 'user:passing-ds',
},
} as never}
routeFileName={null}
config={{ mode: 'daemon', agentId: 'agent-1', notifications: undefined, agentModels: {} } as never}
agents={[{ id: 'agent-1', name: 'OpenCode', models: [] } as never]}
skills={[]}
designTemplates={[]}
designSystems={[]}
daemonLive
onModeChange={() => {}}
onAgentChange={() => {}}
onAgentModelChange={() => {}}
onRefreshAgents={() => {}}
onOpenSettings={() => {}}
onBack={() => {}}
onClearPendingPrompt={() => {}}
onTouchProject={() => {}}
onProjectChange={() => {}}
onProjectsRefresh={() => {}}
/>,
);
await waitFor(() => expect(fetchProjectDesignSystemPackageAudit).toHaveBeenCalledWith('project-ds-pass'));
expect(streamViaDaemon).toHaveBeenCalledTimes(1);
expect(window.sessionStorage.getItem('od:design-system-audit-auto-repair:project-ds-pass')).toBeNull();
});
// Sister check: without the auto-send flag, the composer should still
// seed from pendingPrompt so the user can edit before manually sending.
it('seeds composer initialDraft with pendingPrompt when auto-send flag is absent', async () => {