codex specify
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user