mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Bound design system audit auto repair
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user