codex specify

This commit is contained in:
2026-02-25 21:19:48 +03:00
parent 8731343e52
commit a8ccf6cb79
40 changed files with 3535 additions and 238 deletions

View File

@@ -40,6 +40,7 @@
getAssistantHistory,
getAssistantConversations,
} from "$lib/api/assistant.js";
import { gitService } from "../../../services/gitService.js";
const HISTORY_PAGE_SIZE = 30;
const CONVERSATIONS_PAGE_SIZE = 20;
@@ -385,6 +386,33 @@
return;
}
if (action.type === "open_route" && action.target) {
goto(action.target);
return;
}
if (action.type === "open_diff" && action.target) {
const dashboardId = Number(action.target);
if (!Number.isFinite(dashboardId) || dashboardId <= 0) {
throw new Error("Invalid dashboard id for diff");
}
const diffPayload = await gitService.getDiff(dashboardId);
const diffText =
typeof diffPayload === "string"
? diffPayload
: diffPayload?.diff || JSON.stringify(diffPayload, null, 2);
appendAssistantResponse({
response_id: `diff-${Date.now()}`,
text: diffText
? `Diff для дашборда ${dashboardId}:\n\n${diffText}`
: `Diff для дашборда ${dashboardId} пуст.`,
state: "success",
created_at: new Date().toISOString(),
actions: [],
});
return;
}
if (action.type === "confirm" && message.confirmation_id) {
// Hide buttons immediately to prevent repeated clicks
messages = messages.map((m) =>

View File

@@ -1,50 +1,127 @@
// [DEF:frontend.src.lib.components.assistant.__tests__.assistant_confirmation_integration:Module]
// @TIER: STANDARD
// @SEMANTICS: assistant, confirmation, integration-test, ux
// @PURPOSE: Validate confirm/cancel UX contract bindings in assistant chat panel source.
// @LAYER: UI Tests
// @RELATION: VERIFIES -> frontend/src/lib/components/assistant/AssistantChatPanel.svelte
// @INVARIANT: Confirm/cancel action handling must remain explicit and confirmation-id bound.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import AssistantChatPanel from '../AssistantChatPanel.svelte';
import * as api from '$lib/api/assistant';
import { assistantChatStore } from '$lib/stores/assistantChat';
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
// Mock dependencies
vi.mock('$lib/api/assistant', () => ({
getAssistantHistory: vi.fn(),
confirmAssistantOperation: vi.fn(),
cancelAssistantOperation: vi.fn(),
getAssistantConversations: vi.fn(() => Promise.resolve({ items: [], total: 0 })),
sendAssistantMessage: vi.fn()
}));
const COMPONENT_PATH = path.resolve(
process.cwd(),
'src/lib/components/assistant/AssistantChatPanel.svelte',
);
vi.mock('$lib/toasts', () => ({
addToast: vi.fn()
}));
// [DEF:assistant_confirmation_contract_tests:Function]
// @PURPOSE: Assert that confirmation UX flow and API bindings are preserved in chat panel.
// @PRE: Assistant panel source file exists and is readable.
// @POST: Test guarantees explicit confirm/cancel guards and failed-action recovery path.
describe('AssistantChatPanel confirmation integration contract', () => {
it('contains confirmation action guards with confirmation_id checks', () => {
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
vi.mock('$lib/stores/assistantChat', () => ({
assistantChatStore: {
subscribe: (fn) => {
fn({ isOpen: true, conversationId: 'conv-1' });
return () => { };
}
},
closeAssistantChat: vi.fn(),
setAssistantConversationId: vi.fn()
}));
expect(source).toContain('if (action.type === "confirm" && message.confirmation_id)');
expect(source).toContain('if (action.type === "cancel" && message.confirmation_id)');
expect(source).toContain('confirmAssistantOperation(\n message.confirmation_id,\n )');
expect(source).toContain('cancelAssistantOperation(\n message.confirmation_id,\n )');
vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/i18n', () => ({
t: {
subscribe: (fn) => {
fn({
assistant: {
action_failed: 'Action failed',
confirm: 'Confirm',
cancel: 'Cancel'
}
});
return () => { };
}
}
}));
describe('AssistantChatPanel confirmation functional tests', () => {
const mockMessage = {
id: 'msg-123',
role: 'assistant',
text: 'Confirm migration?',
created_at: new Date().toISOString(),
confirmation: {
id: 'conf-123',
type: 'migration_execute',
status: 'pending'
}
};
beforeEach(() => {
vi.clearAllMocks();
});
it('renders action buttons from assistant response payload', () => {
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
it('renders action buttons and triggers confirm API call', async () => {
// Mock getAssistantHistory to return our message
api.getAssistantHistory.mockResolvedValue({
items: [mockMessage],
total: 1,
has_next: false
});
expect(source).toContain('{#if message.actions?.length}');
expect(source).toContain('{#each message.actions as action}');
expect(source).toContain('{action.label}');
expect(source).toContain('on:click={() => handleAction(action, message)}');
render(AssistantChatPanel);
// Wait for message to render
await waitFor(() => {
expect(screen.getByText('Confirm migration?')).toBeTruthy();
});
const confirmBtn = screen.getByText('Confirm');
expect(confirmBtn).toBeTruthy();
await fireEvent.click(confirmBtn);
expect(api.confirmAssistantOperation).toHaveBeenCalledWith('conf-123');
});
it('keeps failed-action recovery response path', () => {
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
it('triggers cancel API call when cancel button is clicked', async () => {
api.getAssistantHistory.mockResolvedValue({
items: [mockMessage],
total: 1,
has_next: false
});
expect(source).toContain('response_id: `action-error-${Date.now()}`');
expect(source).toContain('state: "failed"');
expect(source).toContain('text: err.message || "Action failed"');
render(AssistantChatPanel);
await waitFor(() => {
expect(screen.getByText('Cancel')).toBeTruthy();
});
const cancelBtn = screen.getByText('Cancel');
await fireEvent.click(cancelBtn);
expect(api.cancelAssistantOperation).toHaveBeenCalledWith('conf-123');
});
it('shows toast error when action fails', async () => {
api.getAssistantHistory.mockResolvedValue({
items: [mockMessage],
total: 1,
has_next: false
});
api.confirmAssistantOperation.mockRejectedValue(new Error('Network error'));
render(AssistantChatPanel);
await waitFor(() => screen.getByText('Confirm'));
await fireEvent.click(screen.getByText('Confirm'));
await waitFor(() => {
// The component appends a failed message to the chat
expect(screen.getAllByText(/Network error/)).toBeTruthy();
});
});
});
// [/DEF:assistant_confirmation_contract_tests:Function]
// [/DEF:frontend.src.lib.components.assistant.__tests__.assistant_confirmation_integration:Module]