feat(FN-1963): merge fusion/fn-1963
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { Plus, Pencil, Trash2, Check, X, GripVertical, Sparkles, Download, Copy, Loader, ChevronLeft, ArrowLeft } from "lucide-react";
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Plus, Pencil, Trash2, Check, X, GripVertical, Sparkles, Download, Copy, Loader, ChevronLeft, ArrowLeft, ChevronUp } from "lucide-react";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useRoadmaps, type FeatureSuggestion, type MilestoneSuggestion, type SuggestionDraftPatch } from "../hooks/useRoadmaps";
|
||||
import { useViewportMode } from "../hooks/useViewportMode";
|
||||
@@ -1456,6 +1456,18 @@ export function RoadmapsView({ projectId, addToast }: RoadmapsViewProps) {
|
||||
// Goal prompt state for milestone suggestion generation
|
||||
const [goalPrompt, setGoalPrompt] = useState("");
|
||||
|
||||
// Mobile suggestion panel collapse state
|
||||
const [showSuggestionPanel, setShowSuggestionPanel] = useState(false);
|
||||
|
||||
// Reset suggestion panel when roadmap changes on mobile
|
||||
const prevRoadmapIdRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (prevRoadmapIdRef.current !== null && prevRoadmapIdRef.current !== selectedRoadmapId) {
|
||||
setShowSuggestionPanel(false);
|
||||
}
|
||||
prevRoadmapIdRef.current = selectedRoadmapId;
|
||||
}, [selectedRoadmapId]);
|
||||
|
||||
// Inline edit states
|
||||
const [roadmapEdit, setRoadmapEdit] = useState<InlineEditState>({
|
||||
roadmapId: null,
|
||||
@@ -2245,67 +2257,154 @@ export function RoadmapsView({ projectId, addToast }: RoadmapsViewProps) {
|
||||
</div>
|
||||
|
||||
{/* Milestone Suggestions Section */}
|
||||
<div className="roadmap-suggestion-section">
|
||||
<div className="roadmap-suggestion-header">
|
||||
<h3 className="roadmap-suggestion-title">Generate Milestone Ideas</h3>
|
||||
</div>
|
||||
<div className="roadmap-suggestion-form">
|
||||
<textarea
|
||||
className="roadmap-suggestion-input"
|
||||
value={goalPrompt}
|
||||
onChange={(e) => setGoalPrompt(e.target.value)}
|
||||
placeholder="Describe your roadmap goal (e.g., 'Build a user authentication system with OAuth, profiles, and admin dashboard')"
|
||||
rows={2}
|
||||
disabled={isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="goal-prompt-input"
|
||||
/>
|
||||
<div className="roadmap-suggestion-actions">
|
||||
<button
|
||||
className="roadmap-suggestion-generate-btn"
|
||||
onClick={handleGenerateSuggestions}
|
||||
disabled={!goalPrompt.trim() || isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="generate-suggestions-btn"
|
||||
>
|
||||
{isGeneratingSuggestions ? "Generating..." : "Generate Milestones"}
|
||||
</button>
|
||||
{isMobile ? (
|
||||
showSuggestionPanel ? (
|
||||
<div className="roadmap-suggestion-section">
|
||||
<div className="roadmap-suggestion-header">
|
||||
<h3 className="roadmap-suggestion-title">Generate Milestone Ideas</h3>
|
||||
<button
|
||||
className="roadmap-suggestion-collapse-btn"
|
||||
onClick={() => setShowSuggestionPanel(false)}
|
||||
aria-label="Collapse suggestion panel"
|
||||
data-testid="collapse-suggestion-panel-btn"
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="roadmap-suggestion-form">
|
||||
<textarea
|
||||
className="roadmap-suggestion-input"
|
||||
value={goalPrompt}
|
||||
onChange={(e) => setGoalPrompt(e.target.value)}
|
||||
placeholder="Describe your roadmap goal (e.g., 'Build a user authentication system with OAuth, profiles, and admin dashboard')"
|
||||
rows={2}
|
||||
disabled={isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="goal-prompt-input"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="roadmap-suggestion-actions">
|
||||
<button
|
||||
className="roadmap-suggestion-generate-btn"
|
||||
onClick={handleGenerateSuggestions}
|
||||
disabled={!goalPrompt.trim() || isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="generate-suggestions-btn"
|
||||
>
|
||||
{isGeneratingSuggestions ? "Generating..." : "Generate Milestones"}
|
||||
</button>
|
||||
{milestoneSuggestions.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className="roadmap-suggestion-accept-all-btn"
|
||||
onClick={handleAcceptAllSuggestions}
|
||||
data-testid="accept-all-suggestions-btn"
|
||||
>
|
||||
Accept All ({milestoneSuggestions.length})
|
||||
</button>
|
||||
<button
|
||||
className="roadmap-suggestion-clear-btn"
|
||||
onClick={handleClearSuggestions}
|
||||
title="Clear suggestions"
|
||||
aria-label="Clear suggestions"
|
||||
data-testid="clear-suggestions-btn"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestion Cards */}
|
||||
{milestoneSuggestions.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className="roadmap-suggestion-accept-all-btn"
|
||||
onClick={handleAcceptAllSuggestions}
|
||||
data-testid="accept-all-suggestions-btn"
|
||||
>
|
||||
Accept All ({milestoneSuggestions.length})
|
||||
</button>
|
||||
<button
|
||||
className="roadmap-suggestion-clear-btn"
|
||||
onClick={handleClearSuggestions}
|
||||
title="Clear suggestions"
|
||||
aria-label="Clear suggestions"
|
||||
data-testid="clear-suggestions-btn"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
<div className="roadmap-suggestion-list">
|
||||
{milestoneSuggestions.map((suggestion) => (
|
||||
<MilestoneSuggestionCard
|
||||
key={suggestion.id}
|
||||
suggestion={suggestion}
|
||||
onUpdateDraft={(patch) => updateMilestoneSuggestionDraft(suggestion.id, patch)}
|
||||
onAccept={() => handleAcceptSuggestion(suggestion.id)}
|
||||
testIdPrefix="suggestion"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestion Cards */}
|
||||
{milestoneSuggestions.length > 0 && (
|
||||
<div className="roadmap-suggestion-list">
|
||||
{milestoneSuggestions.map((suggestion) => (
|
||||
<MilestoneSuggestionCard
|
||||
key={suggestion.id}
|
||||
suggestion={suggestion}
|
||||
onUpdateDraft={(patch) => updateMilestoneSuggestionDraft(suggestion.id, patch)}
|
||||
onAccept={() => handleAcceptSuggestion(suggestion.id)}
|
||||
testIdPrefix="suggestion"
|
||||
/>
|
||||
))}
|
||||
) : (
|
||||
<div className="roadmap-suggestion-section">
|
||||
<button
|
||||
className="roadmap-suggestion-expand-btn"
|
||||
onClick={() => setShowSuggestionPanel(true)}
|
||||
disabled={!selectedRoadmapId}
|
||||
data-testid="expand-suggestion-panel-btn"
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
Generate Milestone Ideas
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="roadmap-suggestion-section">
|
||||
<div className="roadmap-suggestion-header">
|
||||
<h3 className="roadmap-suggestion-title">Generate Milestone Ideas</h3>
|
||||
</div>
|
||||
<div className="roadmap-suggestion-form">
|
||||
<textarea
|
||||
className="roadmap-suggestion-input"
|
||||
value={goalPrompt}
|
||||
onChange={(e) => setGoalPrompt(e.target.value)}
|
||||
placeholder="Describe your roadmap goal (e.g., 'Build a user authentication system with OAuth, profiles, and admin dashboard')"
|
||||
rows={2}
|
||||
disabled={isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="goal-prompt-input"
|
||||
/>
|
||||
<div className="roadmap-suggestion-actions">
|
||||
<button
|
||||
className="roadmap-suggestion-generate-btn"
|
||||
onClick={handleGenerateSuggestions}
|
||||
disabled={!goalPrompt.trim() || isGeneratingSuggestions || !selectedRoadmapId}
|
||||
data-testid="generate-suggestions-btn"
|
||||
>
|
||||
{isGeneratingSuggestions ? "Generating..." : "Generate Milestones"}
|
||||
</button>
|
||||
{milestoneSuggestions.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className="roadmap-suggestion-accept-all-btn"
|
||||
onClick={handleAcceptAllSuggestions}
|
||||
data-testid="accept-all-suggestions-btn"
|
||||
>
|
||||
Accept All ({milestoneSuggestions.length})
|
||||
</button>
|
||||
<button
|
||||
className="roadmap-suggestion-clear-btn"
|
||||
onClick={handleClearSuggestions}
|
||||
title="Clear suggestions"
|
||||
aria-label="Clear suggestions"
|
||||
data-testid="clear-suggestions-btn"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestion Cards */}
|
||||
{milestoneSuggestions.length > 0 && (
|
||||
<div className="roadmap-suggestion-list">
|
||||
{milestoneSuggestions.map((suggestion) => (
|
||||
<MilestoneSuggestionCard
|
||||
key={suggestion.id}
|
||||
suggestion={suggestion}
|
||||
onUpdateDraft={(patch) => updateMilestoneSuggestionDraft(suggestion.id, patch)}
|
||||
onAccept={() => handleAcceptSuggestion(suggestion.id)}
|
||||
testIdPrefix="suggestion"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Milestone lanes */}
|
||||
<div className="roadmaps-view__milestone-lanes">
|
||||
|
||||
@@ -46,6 +46,7 @@ vi.mock("lucide-react", () => ({
|
||||
Loader: (props: unknown) => <span data-testid="loader-icon" {...props}>Loader</span>,
|
||||
ArrowLeft: (props: unknown) => <span data-testid="arrow-left-icon" {...props}>ArrowLeft</span>,
|
||||
ChevronLeft: (props: unknown) => <span data-testid="chevron-left-icon" {...props}>ChevronLeft</span>,
|
||||
ChevronUp: (props: unknown) => <span data-testid="chevron-up-icon" {...props}>ChevronUp</span>,
|
||||
}));
|
||||
|
||||
// Viewport mode mock helper
|
||||
@@ -919,4 +920,180 @@ describe("RoadmapsView", () => {
|
||||
expect(screen.getByText("Milestone 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mobile suggestion panel collapse", () => {
|
||||
beforeEach(() => {
|
||||
mockViewport("mobile");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows expand button instead of suggestion section on mobile", async () => {
|
||||
render(<RoadmapsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select roadmap
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
|
||||
|
||||
// On mobile, should show the expand button instead of the goal prompt input
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("expands suggestion panel on mobile when button is clicked", async () => {
|
||||
render(<RoadmapsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select roadmap
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
|
||||
|
||||
// Expand the panel
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
|
||||
|
||||
// Panel should now be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("expand-suggestion-panel-btn")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can collapse suggestion panel on mobile", async () => {
|
||||
render(<RoadmapsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select roadmap
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
|
||||
|
||||
// Expand the panel
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
|
||||
|
||||
// Wait for panel to expand
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse the panel
|
||||
fireEvent.click(screen.getByTestId("collapse-suggestion-panel-btn"));
|
||||
|
||||
// Panel should be hidden, expand button should be back
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists goal prompt and suggestions across collapse/expand on mobile", async () => {
|
||||
// Mock milestone suggestion generation
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [
|
||||
{ title: "Persisted Milestone", description: "Persisted description" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<RoadmapsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select roadmap
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
|
||||
|
||||
// Expand the panel
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
|
||||
|
||||
// Type into goal prompt
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
|
||||
});
|
||||
const goalInput = screen.getByTestId("goal-prompt-input");
|
||||
await userEvent.type(goalInput, "Build an app");
|
||||
|
||||
// Generate suggestions
|
||||
fireEvent.click(screen.getByTestId("generate-suggestions-btn"));
|
||||
|
||||
// Wait for suggestions to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Persisted Milestone")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse the panel
|
||||
fireEvent.click(screen.getByTestId("collapse-suggestion-panel-btn"));
|
||||
|
||||
// Wait for expand button to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Re-expand the panel
|
||||
fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
|
||||
|
||||
// Goal prompt and suggestions should persist
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("goal-prompt-input")).toHaveValue("Build an app");
|
||||
expect(screen.getByText("Persisted Milestone")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("resets suggestion panel when switching roadmaps on mobile", async () => {
|
||||
render(<RoadmapsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select RM-001
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
|
||||
|
||||
// Expand the panel
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Go back to roadmap list
|
||||
fireEvent.click(screen.getByTestId("mobile-back-btn"));
|
||||
|
||||
// Wait for list to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mobile-roadmap-item-RM-002")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Switch to RM-002
|
||||
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-002"));
|
||||
|
||||
// Panel should be collapsed (expand button visible)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33327,6 +33327,55 @@ html .column.drag-over * {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* === Mobile Suggestion Panel Expand/Collapse === */
|
||||
|
||||
.roadmap-suggestion-expand-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-expand-btn:hover:not(:disabled) {
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-expand-btn:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-expand-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.roadmap-suggestion-collapse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: var(--space-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-collapse-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
/* Allow vertical scrolling on mobile for roadmap list and detail views */
|
||||
@@ -33571,6 +33620,10 @@ html .column.drag-over * {
|
||||
margin: var(--space-md);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-expand-btn {
|
||||
margin: var(--space-md);
|
||||
}
|
||||
|
||||
.roadmap-suggestion-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ export function createMissionRouter(
|
||||
missionStore.updateMission(mission.id, { interviewState: "completed" as InterviewState });
|
||||
|
||||
// Create milestones, slices, and features with verification in dedicated fields.
|
||||
// For each feature, auto-generate a linked contract assertion.
|
||||
// Auto-generate contract assertions for milestone, slice, and feature levels.
|
||||
for (const milestoneData of (summary.milestones ?? [])) {
|
||||
// Use dedicated verification field instead of concatenating into description
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
@@ -744,6 +744,16 @@ export function createMissionRouter(
|
||||
verification: milestoneData.verification,
|
||||
});
|
||||
|
||||
// Milestone-level assertion remains on the milestone even when it has no slices.
|
||||
missionStore.addContractAssertion(milestone.id, {
|
||||
title: `Milestone: ${milestoneData.title}`,
|
||||
assertion:
|
||||
milestoneData.verification
|
||||
|| milestoneData.description
|
||||
|| `Verify milestone completion: ${milestoneData.title}`,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
for (const sliceData of (milestoneData.slices ?? [])) {
|
||||
// Use dedicated verification field instead of concatenating into description
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
@@ -752,6 +762,16 @@ export function createMissionRouter(
|
||||
verification: sliceData.verification,
|
||||
});
|
||||
|
||||
// Slice-level assertion for explicit verification criteria.
|
||||
missionStore.addContractAssertion(milestone.id, {
|
||||
title: `Slice: ${sliceData.title}`,
|
||||
assertion:
|
||||
sliceData.verification
|
||||
|| sliceData.description
|
||||
|| `Verify slice completion: ${sliceData.title}`,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
for (const featureData of (sliceData.features ?? [])) {
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: featureData.title,
|
||||
@@ -759,8 +779,7 @@ export function createMissionRouter(
|
||||
acceptanceCriteria: featureData.acceptanceCriteria,
|
||||
});
|
||||
|
||||
// Auto-generate a contract assertion for this feature
|
||||
// Assertion text source priority: acceptanceCriteria -> description -> fallback
|
||||
// Feature assertion text source priority: acceptanceCriteria -> description -> fallback
|
||||
const assertionText = featureData.acceptanceCriteria
|
||||
|| featureData.description
|
||||
|| `Verify implementation of: ${featureData.title}`;
|
||||
|
||||
@@ -9450,20 +9450,32 @@ describe("POST /api/ai/summarize-title", () => {
|
||||
});
|
||||
|
||||
it("accepts optional provider and modelId parameters", async () => {
|
||||
const fusionCore = await import("@fusion/core");
|
||||
const summarizeTitleSpy = vi
|
||||
.spyOn(fusionCore, "summarizeTitle")
|
||||
.mockResolvedValueOnce("Generated title");
|
||||
|
||||
const description = "x".repeat(300);
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/ai/summarize-title",
|
||||
JSON.stringify({
|
||||
description: "x".repeat(300),
|
||||
description,
|
||||
provider: "google",
|
||||
modelId: "gemini-2.5-pro",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
// Either 200 (success) or 503 (AI service unavailable) is acceptable
|
||||
expect([200, 503]).toContain(res.status);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ title: "Generated title" });
|
||||
expect(summarizeTitleSpy).toHaveBeenCalledWith(
|
||||
description,
|
||||
"/test/project",
|
||||
"google",
|
||||
"gemini-2.5-pro",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user