feat(FN-2924): merge fusion/fn-2924
- Add task provenance display to TaskDetailModal, surfacing AI-generated merge commit summaries so users understand when and why tasks landed - New `ai-summarize` module in core with structured commit summarization logic and tests - Extend CLI `fn task show` with provenance output - Refactor merger to produce cleaner merge commit messages with task context - Update SettingsModal to expose AI summarization toggle (hidden behind feature flag) - Adjust QuickChatFAB styling for theme compatibility - Update scheduler and node-routing policy tests to match new behavior - Add changeset for `@runfusion/fusion` minor release Commits merged: - feat(FN-2924): display task provenance in dashboard and cli - feat(FN-2971): merge fusion/fn-2971 - feat(FN-2947): merge fusion/fn-2947 - feat(FN-2970): merge fusion/fn-2970 - feat(FN-2956): merge fusion/fn-2956 Files changed: .changeset/add-ai-merge-commit-summary.md | 5 + docs/cli-reference.md | 3 +- docs/settings-reference.md | 3 + packages/cli/src/commands/__tests__/task.test.ts | 44 ++++- packages/cli/src/commands/task.ts | 55 +++++++ packages/core/src/__tests__/ai-summarize.test.ts | 64 ++++++++ packages/core/src/ai-summarize.ts | 114 +++++++++++++ packages/core/src/index.ts | 3 + packages/core/src/settings-schema.ts | 1 + packages/core/src/types.ts | 4 + packages/dashboard/app/components/QuickChatFAB.css | 17 +- packages/dashboard/app/components/QuickChatFAB.tsx | 2 +- .../dashboard/app/components/SettingsModal.css | 11 +- .../dashboard/app/components/SettingsModal.tsx | 22 ++- .../dashboard/app/components/TaskDetailModal.css | 40 +++++ .../dashboard/app/components/TaskDetailModal.tsx | 82 +++++++++- .../__tests__/SettingsModalNodeRouting.test.tsx | 12 +- .../components/__tests__/TaskDetailModal.test.tsx | 81 ++++++++- packages/engine/src/__tests__/merger.test.ts | 75 +++++++++ .../src/__tests__/node-routing-policy.test.ts | 25 ++- .../src/__tests__/scheduler-node-routing.test.ts | 18 +- packages/engine/src/merger.ts | 181 +++++++++------------ packages/engine/src/scheduler.ts | 8 +- 23 files changed, 723 insertions(+), 147 deletions(-) Fusion-Task-Id: FN-2924
This commit is contained in:
@@ -313,10 +313,11 @@ fn task show FN-001
|
|||||||
fn task logs FN-001 --follow --limit 50 --type tool
|
fn task logs FN-001 --follow --limit 50 --type tool
|
||||||
```
|
```
|
||||||
|
|
||||||
`fn task show <id>` includes routing context when available:
|
`fn task show <id>` includes routing and provenance context when available:
|
||||||
- task node override
|
- task node override
|
||||||
- project default node fallback
|
- project default node fallback
|
||||||
- unavailable-node policy value
|
- unavailable-node policy value
|
||||||
|
- source provenance line (`Source: <origin>`), including parent task / GitHub issue URL context when present
|
||||||
|
|
||||||
### Execution and status
|
### Execution and status
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ function makeTask(overrides: Record<string, unknown> = {}) {
|
|||||||
describe("runTaskShow", () => {
|
describe("runTaskShow", () => {
|
||||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
|
const mockTaskStoreGetTask = (task: Record<string, unknown>) => {
|
||||||
|
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||||
|
init: vi.fn(),
|
||||||
|
getTask: vi.fn().mockResolvedValue(task),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
@@ -125,10 +132,7 @@ describe("runTaskShow", () => {
|
|||||||
const longDesc = "A".repeat(120); // well over 60 chars
|
const longDesc = "A".repeat(120); // well over 60 chars
|
||||||
const task = makeTask({ description: longDesc });
|
const task = makeTask({ description: longDesc });
|
||||||
|
|
||||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
mockTaskStoreGetTask(task);
|
||||||
init: vi.fn(),
|
|
||||||
getTask: vi.fn().mockResolvedValue(task),
|
|
||||||
}));
|
|
||||||
|
|
||||||
await runTaskShow("FN-001");
|
await runTaskShow("FN-001");
|
||||||
|
|
||||||
@@ -148,10 +152,7 @@ describe("runTaskShow", () => {
|
|||||||
description: "This is the full description that should not appear in the header",
|
description: "This is the full description that should not appear in the header",
|
||||||
});
|
});
|
||||||
|
|
||||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
mockTaskStoreGetTask(task);
|
||||||
init: vi.fn(),
|
|
||||||
getTask: vi.fn().mockResolvedValue(task),
|
|
||||||
}));
|
|
||||||
|
|
||||||
await runTaskShow("FN-001");
|
await runTaskShow("FN-001");
|
||||||
|
|
||||||
@@ -162,6 +163,33 @@ describe("runTaskShow", () => {
|
|||||||
expect(headerLine![0]).toContain("My Task Title");
|
expect(headerLine![0]).toContain("My Task Title");
|
||||||
expect(headerLine![0]).not.toContain("This is the full description");
|
expect(headerLine![0]).not.toContain("This is the full description");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ sourceType: "dashboard_ui" }, "Source: Dashboard"],
|
||||||
|
[{ sourceType: "agent_heartbeat", sourceAgentId: "agent-123" }, "Source: Agent (agent-123)"],
|
||||||
|
[{ sourceType: "task_refine", sourceParentTaskId: "FN-2904" }, "Source: Refinement of FN-2904"],
|
||||||
|
[{ sourceType: "task_duplicate", sourceParentTaskId: "FN-2905" }, "Source: Duplicate of FN-2905"],
|
||||||
|
[
|
||||||
|
{ sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" } },
|
||||||
|
"Source: GitHub Import (https://github.com/owner/repo/issues/42)",
|
||||||
|
],
|
||||||
|
] as const)("prints provenance line for %o", async (overrides, expectedLine) => {
|
||||||
|
mockTaskStoreGetTask(makeTask(overrides));
|
||||||
|
|
||||||
|
await runTaskShow("FN-001");
|
||||||
|
|
||||||
|
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).toContain(expectedLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([{ sourceType: "unknown" }, {}])("omits provenance line for %o", async (overrides) => {
|
||||||
|
mockTaskStoreGetTask(makeTask(overrides));
|
||||||
|
|
||||||
|
await runTaskShow("FN-001");
|
||||||
|
|
||||||
|
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||||
|
expect(output).not.toContain("Source:");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("task node overrides", () => {
|
describe("task node overrides", () => {
|
||||||
|
|||||||
@@ -18,6 +18,57 @@ import { findNodeByNameOrId } from "./node.js";
|
|||||||
|
|
||||||
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
|
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
|
||||||
|
|
||||||
|
function getGitHubIssueUrl(sourceMetadata: unknown): string | undefined {
|
||||||
|
if (!sourceMetadata || typeof sourceMetadata !== "object") return undefined;
|
||||||
|
const issueUrl = (sourceMetadata as { issueUrl?: unknown }).issueUrl;
|
||||||
|
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskSource(task: {
|
||||||
|
sourceType?: string;
|
||||||
|
sourceAgentId?: string;
|
||||||
|
sourceParentTaskId?: string;
|
||||||
|
sourceMetadata?: unknown;
|
||||||
|
}): string | null {
|
||||||
|
switch (task.sourceType) {
|
||||||
|
case "dashboard_ui":
|
||||||
|
return "Dashboard";
|
||||||
|
case "quick_chat":
|
||||||
|
return "Quick Chat";
|
||||||
|
case "chat_session":
|
||||||
|
return "Chat Session";
|
||||||
|
case "agent_heartbeat":
|
||||||
|
return task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent";
|
||||||
|
case "automation":
|
||||||
|
return "Automation";
|
||||||
|
case "cron":
|
||||||
|
return "Scheduled Task";
|
||||||
|
case "workflow_step":
|
||||||
|
return "Workflow Step";
|
||||||
|
case "github_import": {
|
||||||
|
const issueUrl = getGitHubIssueUrl(task.sourceMetadata);
|
||||||
|
return issueUrl ? `GitHub Import (${issueUrl})` : "GitHub Import";
|
||||||
|
}
|
||||||
|
case "task_refine":
|
||||||
|
return task.sourceParentTaskId
|
||||||
|
? `Refinement of ${task.sourceParentTaskId}`
|
||||||
|
: "Refinement";
|
||||||
|
case "task_duplicate":
|
||||||
|
return task.sourceParentTaskId
|
||||||
|
? `Duplicate of ${task.sourceParentTaskId}`
|
||||||
|
: "Duplicate";
|
||||||
|
case "cli":
|
||||||
|
return "CLI";
|
||||||
|
case "api":
|
||||||
|
return "API";
|
||||||
|
case "recovery":
|
||||||
|
return "Recovery";
|
||||||
|
case "unknown":
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface CommandContext {
|
interface CommandContext {
|
||||||
store: TaskStore;
|
store: TaskStore;
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
@@ -519,6 +570,10 @@ export async function runTaskShow(id: string, projectName?: string) {
|
|||||||
if (settings.unavailableNodePolicy) {
|
if (settings.unavailableNodePolicy) {
|
||||||
console.log(` Unavailable Node Policy: ${settings.unavailableNodePolicy}`);
|
console.log(` Unavailable Node Policy: ${settings.unavailableNodePolicy}`);
|
||||||
}
|
}
|
||||||
|
const sourceSummary = formatTaskSource(task);
|
||||||
|
if (sourceSummary) {
|
||||||
|
console.log(` Source: ${sourceSummary}`);
|
||||||
|
}
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// Steps
|
// Steps
|
||||||
|
|||||||
@@ -115,6 +115,46 @@
|
|||||||
margin-bottom: var(--space-lg);
|
margin-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-provenance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-provenance svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-provenance-link {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--todo);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-provenance-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-provenance-link:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-provenance {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.detail-priority-chip {
|
.detail-priority-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./TaskDetailModal.css";
|
import "./TaskDetailModal.css";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Pencil, Bot, X, ChevronDown, ChevronRight } from "lucide-react";
|
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
||||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
@@ -222,6 +222,64 @@ function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "sta
|
|||||||
return executionMode === "fast" ? "fast" : "standard";
|
return executionMode === "fast" ? "fast" : "standard";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProvenanceDisplay {
|
||||||
|
label: string;
|
||||||
|
parentTaskId?: string;
|
||||||
|
contextInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | undefined {
|
||||||
|
const issueUrl = metadata?.issueUrl;
|
||||||
|
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvenanceLabel(task: Task | TaskDetail): ProvenanceDisplay | null {
|
||||||
|
switch (task.sourceType) {
|
||||||
|
case "dashboard_ui":
|
||||||
|
return { label: "Dashboard" };
|
||||||
|
case "quick_chat":
|
||||||
|
return { label: "Quick Chat" };
|
||||||
|
case "chat_session":
|
||||||
|
return { label: "Chat Session" };
|
||||||
|
case "agent_heartbeat":
|
||||||
|
return {
|
||||||
|
label: task.sourceAgentId ? `Agent (${task.sourceAgentId})` : "Agent",
|
||||||
|
};
|
||||||
|
case "automation":
|
||||||
|
return { label: "Automation" };
|
||||||
|
case "cron":
|
||||||
|
return { label: "Scheduled Task" };
|
||||||
|
case "workflow_step":
|
||||||
|
return { label: "Workflow Step" };
|
||||||
|
case "github_import": {
|
||||||
|
const issueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||||
|
return {
|
||||||
|
label: "GitHub Import",
|
||||||
|
contextInfo: issueUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "task_refine":
|
||||||
|
return {
|
||||||
|
label: "Refinement",
|
||||||
|
parentTaskId: task.sourceParentTaskId,
|
||||||
|
};
|
||||||
|
case "task_duplicate":
|
||||||
|
return {
|
||||||
|
label: "Duplicate",
|
||||||
|
parentTaskId: task.sourceParentTaskId,
|
||||||
|
};
|
||||||
|
case "cli":
|
||||||
|
return { label: "CLI" };
|
||||||
|
case "api":
|
||||||
|
return { label: "API" };
|
||||||
|
case "recovery":
|
||||||
|
return { label: "Recovery" };
|
||||||
|
case "unknown":
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DESCRIPTION_TRUNCATE_LENGTH = 200;
|
const DESCRIPTION_TRUNCATE_LENGTH = 200;
|
||||||
|
|
||||||
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
|
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
|
||||||
@@ -301,6 +359,7 @@ export function TaskDetailModal({
|
|||||||
(task.stuckKillCount ?? 0) > 0 ||
|
(task.stuckKillCount ?? 0) > 0 ||
|
||||||
(task.recoveryRetryCount ?? 0) > 0 ||
|
(task.recoveryRetryCount ?? 0) > 0 ||
|
||||||
Boolean(task.nextRecoveryAt);
|
Boolean(task.nextRecoveryAt);
|
||||||
|
const provenanceDisplay = getProvenanceLabel(workingTask);
|
||||||
|
|
||||||
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1488,6 +1547,27 @@ export function TaskDetailModal({
|
|||||||
<span className={`detail-priority-chip detail-priority-chip--${normalizeTaskPriorityValue(task.priority)}`}>
|
<span className={`detail-priority-chip detail-priority-chip--${normalizeTaskPriorityValue(task.priority)}`}>
|
||||||
Priority: {normalizeTaskPriorityValue(task.priority)}
|
Priority: {normalizeTaskPriorityValue(task.priority)}
|
||||||
</span>
|
</span>
|
||||||
|
{provenanceDisplay && (
|
||||||
|
<div className="detail-provenance">
|
||||||
|
<GitBranch aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
Created via {provenanceDisplay.label}
|
||||||
|
{provenanceDisplay.parentTaskId && (
|
||||||
|
<>
|
||||||
|
{" "}of{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="detail-provenance-link"
|
||||||
|
onClick={() => handleDepClick(provenanceDisplay.parentTaskId!)}
|
||||||
|
>
|
||||||
|
{provenanceDisplay.parentTaskId}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{provenanceDisplay.contextInfo ? ` (${provenanceDisplay.contextInfo})` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ vi.mock("../../api", () => ({
|
|||||||
uploadAttachment: vi.fn(),
|
uploadAttachment: vi.fn(),
|
||||||
deleteAttachment: vi.fn(),
|
deleteAttachment: vi.fn(),
|
||||||
updateTask: vi.fn().mockResolvedValue({}),
|
updateTask: vi.fn().mockResolvedValue({}),
|
||||||
fetchTaskDetail: vi.fn(),
|
fetchTaskDetail: vi.fn().mockResolvedValue(makeTask()),
|
||||||
fetchAgentLogs: vi.fn().mockResolvedValue([]),
|
fetchAgentLogs: vi.fn().mockResolvedValue([]),
|
||||||
requestSpecRevision: vi.fn().mockResolvedValue({}),
|
requestSpecRevision: vi.fn().mockResolvedValue({}),
|
||||||
approvePlan: vi.fn().mockResolvedValue({}),
|
approvePlan: vi.fn().mockResolvedValue({}),
|
||||||
@@ -57,6 +57,7 @@ vi.mock("lucide-react", () => ({
|
|||||||
CircleDot: () => null,
|
CircleDot: () => null,
|
||||||
XCircle: () => null,
|
XCircle: () => null,
|
||||||
GitMerge: () => null,
|
GitMerge: () => null,
|
||||||
|
GitBranch: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../hooks/useAgentLogs", () => ({
|
vi.mock("../../hooks/useAgentLogs", () => ({
|
||||||
@@ -136,6 +137,84 @@ describe("TaskDetailModal", () => {
|
|||||||
localStorage.removeItem("fn.authToken");
|
localStorage.removeItem("fn.authToken");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("provenance display", () => {
|
||||||
|
it.each([
|
||||||
|
["dashboard_ui", undefined, "Created via Dashboard"],
|
||||||
|
["agent_heartbeat", "agent-123", "Created via Agent (agent-123)"],
|
||||||
|
] as const)("renders provenance text for %s", (sourceType, sourceAgentId, expectedText) => {
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ sourceType, sourceAgentId })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders parent task link for refinement provenance", async () => {
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ sourceType: "task_refine", sourceParentTaskId: "FN-001" })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Created via Refinement/)).toBeInTheDocument();
|
||||||
|
const link = screen.getByRole("button", { name: "FN-001" });
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
await userEvent.click(link);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(noopOpenDetail).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders issue URL for github import provenance", () => {
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" },
|
||||||
|
})}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Created via GitHub Import (https://github.com/owner/repo/issues/42)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["unknown", undefined] as const)("omits provenance for %s source", (sourceType) => {
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ sourceType })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/Created via/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("styles detail-body scrollbar rules", () => {
|
it("styles detail-body scrollbar rules", () => {
|
||||||
const css = loadAllAppCss();
|
const css = loadAllAppCss();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user