mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
Load file drawer content from file roots
This commit is contained in:
1 parent
4ad5078224
commit
7284cda909
8 files changed
+380
-14
No files matched your search
@@ -1,13 +1,57 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { AgentSpecReview } from "../hooks/useSpecReview.js";
|
||||
import { WorkflowCodePreview } from "./WorkflowScaffold.js";
|
||||
import { FileReferenceTrigger } from "./drawer-triggers/FileReferenceTrigger.js";
|
||||
|
||||
interface AgentSpecDisplayProps {
|
||||
review?: AgentSpecReview | null;
|
||||
yaml: string;
|
||||
testIdPrefix?: string;
|
||||
sourcePath?: string | null;
|
||||
}
|
||||
|
||||
export function AgentSpecDisplay({ review, yaml, testIdPrefix = "agent" }: AgentSpecDisplayProps) {
|
||||
function resolveSpecRelativePath(sourcePath: string | null | undefined, filePath: string): string | null {
|
||||
if (!sourcePath || !filePath) return null;
|
||||
if (filePath.startsWith("/")) return filePath;
|
||||
const sourceDir = sourcePath.replace(/\/[^/]*$/, "");
|
||||
const parts = `${sourceDir}/${filePath}`.split("/");
|
||||
const resolved: string[] = [];
|
||||
for (const part of parts) {
|
||||
if (!part || part === ".") continue;
|
||||
if (part === "..") {
|
||||
resolved.pop();
|
||||
continue;
|
||||
}
|
||||
resolved.push(part);
|
||||
}
|
||||
return `/${resolved.join("/")}`;
|
||||
}
|
||||
|
||||
function FileChip({
|
||||
label,
|
||||
sourcePath,
|
||||
testId,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
sourcePath?: string | null;
|
||||
testId: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const absolutePath = resolveSpecRelativePath(sourcePath, label);
|
||||
if (!absolutePath) return <>{children}</>;
|
||||
return (
|
||||
<FileReferenceTrigger
|
||||
data={{ path: label, absolutePath }}
|
||||
testId={testId}
|
||||
className="inline-block text-left"
|
||||
>
|
||||
{children}
|
||||
</FileReferenceTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentSpecDisplay({ review, yaml, testIdPrefix = "agent", sourcePath }: AgentSpecDisplayProps) {
|
||||
const profiles = review?.profiles ?? [];
|
||||
const resources = review?.resources ?? { skills: [], guidance: [], hooks: [], subagents: [] };
|
||||
const startup = review?.startup ?? { files: [], actions: [] };
|
||||
@@ -45,7 +89,16 @@ export function AgentSpecDisplay({ review, yaml, testIdPrefix = "agent" }: Agent
|
||||
<div>
|
||||
<span className="text-stone-500">Guidance:</span>{" "}
|
||||
{resources.guidance.map((g, i) => (
|
||||
<span key={i} className="inline-block bg-stone-100 px-1.5 py-0.5 mr-1 mb-0.5">{g}</span>
|
||||
<FileChip
|
||||
key={`${g}-${i}`}
|
||||
label={g}
|
||||
sourcePath={sourcePath}
|
||||
testId={`${testIdPrefix}-guidance-file-trigger-${g}`}
|
||||
>
|
||||
<span className="inline-block bg-stone-100 px-1.5 py-0.5 mr-1 mb-0.5 underline decoration-dotted decoration-stone-400">
|
||||
{g}
|
||||
</span>
|
||||
</FileChip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -66,7 +119,14 @@ export function AgentSpecDisplay({ review, yaml, testIdPrefix = "agent" }: Agent
|
||||
<div className="font-mono text-[9px] text-stone-500 uppercase mb-1">Files</div>
|
||||
{startup.files.map((f, i) => (
|
||||
<div key={i} className="font-mono text-[10px]">
|
||||
{f.path} {f.required && <span className="text-red-500 text-[8px]">REQUIRED</span>}
|
||||
<FileChip
|
||||
label={f.path}
|
||||
sourcePath={sourcePath}
|
||||
testId={`${testIdPrefix}-startup-file-trigger-${f.path}`}
|
||||
>
|
||||
<span className="underline decoration-dotted decoration-stone-400">{f.path}</span>
|
||||
</FileChip>{" "}
|
||||
{f.required && <span className="text-red-500 text-[8px]">REQUIRED</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,12 @@ function LibraryAgentReviewPage({ review }: { review: LibraryAgentReview }) {
|
||||
<WorkflowSummaryCard label="Skills" value={resources.skills.length} testId="lib-agent-skills" />
|
||||
</WorkflowSummaryGrid>
|
||||
|
||||
<AgentSpecDisplay review={review} yaml={review.raw} testIdPrefix="lib-agent" />
|
||||
<AgentSpecDisplay
|
||||
review={review}
|
||||
yaml={review.raw}
|
||||
testIdPrefix="lib-agent"
|
||||
sourcePath={review.sourcePath}
|
||||
/>
|
||||
</div>
|
||||
</WorkspacePage>
|
||||
);
|
||||
@@ -639,7 +644,7 @@ function ContextPackReviewBody({
|
||||
className={`font-mono text-[10px] ${missing ? "text-red-700" : "text-stone-800"}`}
|
||||
>
|
||||
<FileReferenceTrigger
|
||||
data={{ path: f.path }}
|
||||
data={{ path: f.path, absolutePath: f.absolutePath }}
|
||||
testId={`lib-pack-file-trigger-${f.path}`}
|
||||
className="block w-full px-3 py-2 text-left hover:bg-stone-100/60 transition-colors"
|
||||
>
|
||||
@@ -906,7 +911,7 @@ function AgentImageReviewBody({
|
||||
data-testid={`lib-image-file-${f.path}`}
|
||||
>
|
||||
<FileReferenceTrigger
|
||||
data={{ path: f.path }}
|
||||
data={{ path: f.path, absolutePath: f.absolutePath }}
|
||||
testId={`lib-image-file-trigger-${f.path}`}
|
||||
className="block w-full px-3 py-2 text-left hover:bg-stone-100/60 transition-colors"
|
||||
>
|
||||
|
||||
@@ -101,7 +101,12 @@ function AgentSpecSection({ data }: { data: NodeDetailData }) {
|
||||
) : !review || review.kind !== "agent" ? (
|
||||
<div data-testid="agent-spec-unavailable" className="p-4 font-mono text-[10px] text-stone-400">No agent spec available</div>
|
||||
) : (
|
||||
<AgentSpecDisplay review={review as AgentSpecReview} yaml={review.raw} testIdPrefix="live-agent" />
|
||||
<AgentSpecDisplay
|
||||
review={review as AgentSpecReview}
|
||||
yaml={review.raw}
|
||||
testIdPrefix="live-agent"
|
||||
sourcePath={review.sourcePath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -460,7 +465,7 @@ function StartupTab({ rigId, logicalId, data }: { rigId: string; logicalId: stri
|
||||
data-testid={`live-startup-file-${f.path}`}
|
||||
>
|
||||
<FileReferenceTrigger
|
||||
data={{ path: f.path }}
|
||||
data={{ path: f.path, absolutePath: f.absolutePath }}
|
||||
testId={`live-startup-file-trigger-${f.path}`}
|
||||
className="block w-full px-3 py-2 text-left hover:bg-stone-100/60 transition-colors font-mono text-[10px]"
|
||||
>
|
||||
|
||||
@@ -2,17 +2,36 @@
|
||||
//
|
||||
// Renders markdown / text / YAML / JSON / image / binary file refs.
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { SectionHeader } from "../ui/section-header.js";
|
||||
import { EmptyState } from "../ui/empty-state.js";
|
||||
import { MarkdownViewer } from "../markdown/MarkdownViewer.js";
|
||||
import {
|
||||
fileAssetUrl,
|
||||
useFilesRead,
|
||||
useFilesRoots,
|
||||
type AllowlistRoot,
|
||||
} from "../../hooks/useFiles.js";
|
||||
|
||||
export type FileKind = "markdown" | "text" | "yaml" | "json" | "image" | "binary";
|
||||
|
||||
export interface FileViewerData {
|
||||
/** Display path shown in the drawer. Also used as the relative read path when `root` is set. */
|
||||
path: string;
|
||||
kind?: FileKind;
|
||||
content?: string;
|
||||
imageUrl?: string;
|
||||
/** Existing /api/files allowlist root name. */
|
||||
root?: string;
|
||||
/** Optional explicit path under `root`; falls back to `path` when omitted. */
|
||||
readPath?: string;
|
||||
/** Absolute file path; resolved against /api/files/roots before reading. */
|
||||
absolutePath?: string | null;
|
||||
}
|
||||
|
||||
interface ResolvedReadTarget {
|
||||
root: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function inferKind(path: string): FileKind {
|
||||
@@ -25,9 +44,66 @@ function inferKind(path: string): FileKind {
|
||||
return "text";
|
||||
}
|
||||
|
||||
export function FileViewer({ path, kind, content, imageUrl }: FileViewerData) {
|
||||
const resolvedKind = kind ?? inferKind(path);
|
||||
function normalizeAbsolutePath(path: string): string {
|
||||
return path.replaceAll("\\", "/").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function resolveFromAbsolutePath(
|
||||
roots: AllowlistRoot[] | undefined,
|
||||
absolutePath: string | null | undefined,
|
||||
): ResolvedReadTarget | null {
|
||||
if (!roots || !absolutePath) return null;
|
||||
const normalizedFile = normalizeAbsolutePath(absolutePath);
|
||||
const sortedRoots = [...roots].sort((a, b) => b.path.length - a.path.length);
|
||||
for (const root of sortedRoots) {
|
||||
const normalizedRoot = normalizeAbsolutePath(root.path);
|
||||
if (normalizedFile === normalizedRoot) return null;
|
||||
const prefix = `${normalizedRoot}/`;
|
||||
if (!normalizedFile.startsWith(prefix)) continue;
|
||||
const relativePath = normalizedFile.slice(prefix.length);
|
||||
if (!relativePath || relativePath.includes("../")) return null;
|
||||
return { root: root.name, path: relativePath };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function useResolvedReadTarget(data: FileViewerData): {
|
||||
rootsLoading: boolean;
|
||||
target: ResolvedReadTarget | null;
|
||||
hasFetchIntent: boolean;
|
||||
} {
|
||||
const explicitTarget = data.root
|
||||
? { root: data.root, path: data.readPath ?? data.path }
|
||||
: null;
|
||||
const needsRootResolution = !explicitTarget && !!data.absolutePath;
|
||||
const roots = useFilesRoots();
|
||||
const absoluteTarget = useMemo(() => {
|
||||
if (!needsRootResolution) return null;
|
||||
const rootData = roots.data;
|
||||
if (!rootData || "unavailable" in rootData) return null;
|
||||
return resolveFromAbsolutePath(rootData.roots, data.absolutePath);
|
||||
}, [data.absolutePath, needsRootResolution, roots.data]);
|
||||
|
||||
return {
|
||||
rootsLoading: needsRootResolution && roots.isLoading,
|
||||
target: explicitTarget ?? absoluteTarget,
|
||||
hasFetchIntent: !!explicitTarget || !!data.absolutePath,
|
||||
};
|
||||
}
|
||||
|
||||
function FileViewerBody({
|
||||
path,
|
||||
resolvedKind,
|
||||
content,
|
||||
imageUrl,
|
||||
target,
|
||||
}: {
|
||||
path: string;
|
||||
resolvedKind: FileKind;
|
||||
content?: string;
|
||||
imageUrl?: string;
|
||||
target?: ResolvedReadTarget | null;
|
||||
}) {
|
||||
if (!content && !imageUrl && resolvedKind !== "binary") {
|
||||
return (
|
||||
<EmptyState
|
||||
@@ -44,6 +120,11 @@ export function FileViewer({ path, kind, content, imageUrl }: FileViewerData) {
|
||||
<header className="px-4 py-3 border-b border-outline-variant">
|
||||
<SectionHeader tone="muted">File</SectionHeader>
|
||||
<h3 className="mt-1 font-mono text-xs text-stone-900 break-all">{path}</h3>
|
||||
{target && (
|
||||
<div data-testid="file-viewer-root-path" className="mt-1 font-mono text-[9px] text-stone-500 break-all">
|
||||
{target.root}/{target.path}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
{resolvedKind === "markdown" && content ? (
|
||||
@@ -80,3 +161,73 @@ export function FileViewer({ path, kind, content, imageUrl }: FileViewerData) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileViewerWithFetch(data: FileViewerData) {
|
||||
const { path, kind, content, imageUrl } = data;
|
||||
const resolvedKind = kind ?? inferKind(path);
|
||||
const { rootsLoading, target, hasFetchIntent } = useResolvedReadTarget(data);
|
||||
const read = useFilesRead(target?.root ?? null, target?.path ?? null);
|
||||
const fetchedContent = read.data?.content;
|
||||
const fetchedImageUrl = target && resolvedKind === "image"
|
||||
? fileAssetUrl(target.root, target.path)
|
||||
: undefined;
|
||||
const resolvedContent = content ?? fetchedContent;
|
||||
const resolvedImageUrl = imageUrl ?? fetchedImageUrl;
|
||||
|
||||
if (!content && !imageUrl && hasFetchIntent && (rootsLoading || read.isLoading)) {
|
||||
return (
|
||||
<EmptyState
|
||||
label="LOADING"
|
||||
description={`Loading ${path}...`}
|
||||
variant="card"
|
||||
testId="file-viewer-empty"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!content && !imageUrl && hasFetchIntent && !target) {
|
||||
return (
|
||||
<EmptyState
|
||||
label="FILE UNAVAILABLE"
|
||||
description={`No configured file root contains ${path}.`}
|
||||
variant="card"
|
||||
testId="file-viewer-error"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!content && !imageUrl && read.isError) {
|
||||
return (
|
||||
<EmptyState
|
||||
label="FILE UNAVAILABLE"
|
||||
description={(read.error as Error)?.message ?? `Could not load ${path}.`}
|
||||
variant="card"
|
||||
testId="file-viewer-error"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileViewerBody
|
||||
path={path}
|
||||
resolvedKind={resolvedKind}
|
||||
content={resolvedContent}
|
||||
imageUrl={resolvedImageUrl}
|
||||
target={target}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileViewer(data: FileViewerData) {
|
||||
if (!data.root && !data.absolutePath) {
|
||||
return (
|
||||
<FileViewerBody
|
||||
path={data.path}
|
||||
resolvedKind={data.kind ?? inferKind(data.path)}
|
||||
content={data.content}
|
||||
imageUrl={data.imageUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <FileViewerWithFetch {...data} />;
|
||||
}
|
||||
@@ -53,7 +53,13 @@ export interface NodeDetailData {
|
||||
resolvedSpecName: string | null;
|
||||
resolvedSpecVersion: string | null;
|
||||
cwd: string | null;
|
||||
startupFiles: Array<{ path: string; deliveryHint: string; required: boolean }>;
|
||||
startupFiles: Array<{
|
||||
path: string;
|
||||
deliveryHint: string;
|
||||
required: boolean;
|
||||
absolutePath?: string | null;
|
||||
ownerRoot?: string | null;
|
||||
}>;
|
||||
startupActions: Array<{ type: string; value: string }>;
|
||||
recentEvents: Array<{ type: string; createdAt: string }>;
|
||||
infrastructureStartupCommand: string | null;
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
// guard lives in test/node-selection-migration.test.tsx.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { render, fireEvent, cleanup, screen } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import {
|
||||
DrawerSelectionContext,
|
||||
@@ -34,8 +36,20 @@ import { SharedDetailDrawer } from "../src/components/SharedDetailDrawer.js";
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderWithQuery(ui: ReactElement) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Viewers — render-without-crash with canonical-shape props
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -83,6 +97,72 @@ describe("Drawer viewers (P4-1) render with canonical props", () => {
|
||||
expect(getByTestId("file-viewer-empty")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("FileViewer reads drawer content from an explicit /api/files root + path", async () => {
|
||||
const fetchMock = vi.fn(async (url: string) => {
|
||||
expect(url).toBe("/api/files/read?root=workspace&path=docs%2Frole.md");
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
root: "workspace",
|
||||
path: "docs/role.md",
|
||||
absolutePath: "/workspace/docs/role.md",
|
||||
content: "# Role\nLoaded from files API.",
|
||||
mtime: "2026-05-07T00:00:00.000Z",
|
||||
contentHash: "hash",
|
||||
size: 29,
|
||||
truncated: false,
|
||||
truncatedAtBytes: null,
|
||||
totalBytes: 29,
|
||||
}),
|
||||
};
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
renderWithQuery(
|
||||
<FileViewer path="role.md" kind="markdown" root="workspace" readPath="docs/role.md" />,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId("file-viewer")).toBeTruthy();
|
||||
expect(screen.getByText(/Loaded from files API/)).toBeTruthy();
|
||||
expect(screen.getByTestId("file-viewer-root-path").textContent).toBe("workspace/docs/role.md");
|
||||
});
|
||||
|
||||
it("FileViewer resolves an absolute file path against /api/files roots", async () => {
|
||||
const fetchMock = vi.fn(async (url: string) => {
|
||||
if (url === "/api/files/roots") {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ roots: [{ name: "workspace", path: "/workspace" }] }),
|
||||
};
|
||||
}
|
||||
expect(url).toBe("/api/files/read?root=workspace&path=agents%2Frole.md");
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
root: "workspace",
|
||||
path: "agents/role.md",
|
||||
absolutePath: "/workspace/agents/role.md",
|
||||
content: "# Agent Role",
|
||||
mtime: "2026-05-07T00:00:00.000Z",
|
||||
contentHash: "hash",
|
||||
size: 12,
|
||||
truncated: false,
|
||||
truncatedAtBytes: null,
|
||||
totalBytes: 12,
|
||||
}),
|
||||
};
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
renderWithQuery(
|
||||
<FileViewer path="role.md" kind="markdown" absolutePath="/workspace/agents/role.md" />,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId("file-viewer")).toBeTruthy();
|
||||
expect(screen.getByText(/Agent Role/)).toBeTruthy();
|
||||
expect(screen.getByTestId("file-viewer-root-path").textContent).toBe("workspace/agents/role.md");
|
||||
});
|
||||
|
||||
it("SubSpecPreview renders header + manifest excerpt; no Link when entryId omitted", () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<SubSpecPreview
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
|
||||
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { createTestRouter } from "./helpers/test-router.js";
|
||||
import { LiveNodeDetails } from "../src/components/LiveNodeDetails.js";
|
||||
import { DrawerSelectionContext, type DrawerSelection } from "../src/components/AppShell.js";
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
@@ -12,7 +13,12 @@ const NODE_DETAIL = {
|
||||
tmuxAttachCommand: "tmux attach -t dev-impl@test-rig", resumeCommand: null,
|
||||
latestError: null, model: "opus", agentRef: "local:agents/impl", profile: "default",
|
||||
resolvedSpecName: "impl", resolvedSpecVersion: "1.0.0", cwd: "/workspace",
|
||||
startupFiles: [{ path: "role.md", deliveryHint: "guidance_merge", required: true }],
|
||||
startupFiles: [{
|
||||
path: "role.md",
|
||||
deliveryHint: "guidance_merge",
|
||||
required: true,
|
||||
absolutePath: "/workspace/specs/agents/impl/guidance/role.md",
|
||||
}],
|
||||
startupActions: [], recentEvents: [],
|
||||
infrastructureStartupCommand: null,
|
||||
binding: { tmuxSession: "dev-impl@test-rig" },
|
||||
@@ -77,6 +83,19 @@ describe("LiveNodeDetails", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function renderDetailsWithDrawerSelection(setSelection: (sel: DrawerSelection) => void, logicalId = "dev.impl") {
|
||||
return render(
|
||||
createTestRouter({
|
||||
component: () => (
|
||||
<DrawerSelectionContext.Provider value={{ selection: null, setSelection }}>
|
||||
<LiveNodeDetails rigId="rig-1" logicalId={logicalId} />
|
||||
</DrawerSelectionContext.Provider>
|
||||
),
|
||||
path: "/test",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it("renders with identity section showing peers and edges", async () => {
|
||||
mockNodeDetail(NODE_DETAIL);
|
||||
renderDetails();
|
||||
@@ -142,6 +161,23 @@ describe("LiveNodeDetails", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("startup file trigger includes resolved file provenance for drawer loading", async () => {
|
||||
const setSelection = vi.fn();
|
||||
mockNodeDetail(NODE_DETAIL);
|
||||
renderDetailsWithDrawerSelection(setSelection as (sel: DrawerSelection) => void);
|
||||
|
||||
fireEvent.click(await screen.findByTestId("live-tab-startup"));
|
||||
fireEvent.click(await screen.findByTestId("live-startup-file-trigger-role.md"));
|
||||
|
||||
expect(setSelection).toHaveBeenCalledWith({
|
||||
type: "file",
|
||||
data: {
|
||||
path: "role.md",
|
||||
absolutePath: "/workspace/specs/agents/impl/guidance/role.md",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("transcript and terminal tabs own their named content", async () => {
|
||||
mockNodeDetail(NODE_DETAIL);
|
||||
renderDetails();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { RigSpecDisplay } from "../src/components/RigSpecDisplay.js";
|
||||
import { AgentSpecDisplay } from "../src/components/AgentSpecDisplay.js";
|
||||
import { DrawerSelectionContext, type DrawerSelection } from "../src/components/AppShell.js";
|
||||
import type { RigSpecReview, AgentSpecReview } from "../src/hooks/useSpecReview.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
@@ -159,6 +160,28 @@ describe("AgentSpecDisplay", () => {
|
||||
expect(screen.getByText("code-style.md")).toBeDefined();
|
||||
});
|
||||
|
||||
it("wraps guidance resources in file triggers when sourcePath is available", () => {
|
||||
const setSelection = vi.fn();
|
||||
render(
|
||||
<DrawerSelectionContext.Provider value={{ selection: null, setSelection: setSelection as (sel: DrawerSelection) => void }}>
|
||||
<AgentSpecDisplay
|
||||
review={AGENT_REVIEW}
|
||||
yaml="name: test"
|
||||
sourcePath="/workspace/specs/agents/test-agent/agent.yaml"
|
||||
/>
|
||||
</DrawerSelectionContext.Provider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-guidance-file-trigger-code-style.md"));
|
||||
expect(setSelection).toHaveBeenCalledWith({
|
||||
type: "file",
|
||||
data: {
|
||||
path: "code-style.md",
|
||||
absolutePath: "/workspace/specs/agents/test-agent/code-style.md",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("renders startup section with files and actions", () => {
|
||||
render(<AgentSpecDisplay review={AGENT_REVIEW} yaml="name: test" />);
|
||||
|
||||
|
||||
Reference in new issue
Block a user