Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,36 @@ describe("ReportAuditView", () => {
expect(markup).toContain("尚未进入当前公开快照");
expect(markup).not.toContain("匹配任务");
});

it("renders sanitized producer context when a completed job exposes runtime tails", () => {
const markup = renderToStaticMarkup(
<ReportAuditView
locale="en-US"
rows={rows}
provenance={{}}
historyPlaceholder="No history data"
producerContext={{
status: "completed",
updatedAt: "2026-05-15T08:12:30Z",
stdoutTail: "saved C:\\runtime\\private\\score.json\nreported token=abc123",
stderrTail: "upload to http://localhost:8780 failed",
stateHistory: [
{ state: "queued", timestamp: "2026-05-15T08:12:00Z" },
{ state: "completed", timestamp: "2026-05-15T08:12:30Z" },
],
}}
/>,
);

expect(markup).toContain("Producer context");
expect(markup).toContain("Completed");
expect(markup).toContain("Runtime output tail");
expect(markup).toContain("State history");
expect(markup).toContain("&lt;local-path&gt;");
expect(markup).toContain("token=&lt;redacted&gt;");
expect(markup).toContain("&lt;runtime-url&gt;");
expect(markup).not.toContain("C:\\runtime\\private");
expect(markup).not.toContain("abc123");
expect(markup).not.toContain("localhost:8780");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { ReportEvidenceStack } from "@/components/report-evidence-stack";
import { MetricTooltip } from "@/components/metric-tooltip";
import { type Locale } from "@/components/language-picker";
import { type AttackDefenseRowViewModel } from "@/lib/workspace-source";
import { formatFullTime } from "@/lib/format";
import { riskLabel } from "@/lib/risk-report";
import { sanitizeRuntimeText } from "@/lib/runtime-text";
import { WORKSPACE_COPY } from "@/lib/workspace-copy";

export type ReportProvenance = {
Expand All @@ -26,25 +28,56 @@ export type ReportJobContext = {
aucLabel?: string;
};

export type ReportProducerContext = {
status?: string | null;
updatedAt?: string | null;
stdoutTail?: string | null;
stderrTail?: string | null;
stateHistory?: Array<{
state: string;
timestamp?: string | null;
}>;
};

type ReportAuditViewProps = {
locale: Locale;
rows: AttackDefenseRowViewModel[];
provenance: ReportProvenance;
historyPlaceholder: string;
jobContext?: ReportJobContext;
producerContext?: ReportProducerContext;
highlightedRowKeys?: string[];
};

function hasValue(value?: string | null): boolean {
return !!value && value.trim().length > 0 && value.trim() !== "—";
}

function displayStatus(value?: string | null) {
if (!value) {
return undefined;
}

const normalized = value.trim();
if (!normalized) {
return undefined;
}

return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}

function sanitizedTail(value?: string | null) {
const sanitized = sanitizeRuntimeText(value);
return sanitized?.trim() ? sanitized : undefined;
}

export function ReportAuditView({
locale,
rows,
provenance,
historyPlaceholder,
jobContext,
producerContext,
highlightedRowKeys = [],
}: ReportAuditViewProps) {
const copy = WORKSPACE_COPY[locale].reports;
Expand All @@ -69,6 +102,11 @@ export function ReportAuditView({
historyTitle: "历史对照",
historyPlaceholder:
"历史对照数据将在后续版本接入,敬请期待。",
producerTitle: "生产者上下文",
producerStatus: "状态",
producerUpdated: "更新时间",
outputTail: "Runtime 输出尾部",
stateHistory: "状态历史",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback string "Unknown" used for state history entries is hardcoded. It should be localized within the t translation object to maintain consistency with the rest of the UI.

Suggested change
stateHistory: "状态历史",
stateHistory: "状态历史",
unknownState: "未知状态",

noProvenanceData: "暂无溯源数据。",
}
: {
Expand All @@ -90,10 +128,26 @@ export function ReportAuditView({
historyTitle: "History comparison",
historyPlaceholder:
"Historical comparison data will be available in a future release.",
producerTitle: "Producer context",
producerStatus: "Status",
producerUpdated: "Updated",
outputTail: "Runtime output tail",
stateHistory: "State history",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback string "Unknown" used for state history entries is hardcoded. It should be localized within the t translation object.

Suggested change
stateHistory: "State history",
stateHistory: "State history",
unknownState: "Unknown",

noProvenanceData: "No provenance data available.",
};
const defendedRows = rows.filter((row) => row.defense !== "none" && row.defense !== "None").length;
const undefendedRows = rows.length - defendedRows;
const producerStatus = displayStatus(producerContext?.status);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The producerStatus is derived from producerContext?.status without sanitization, while other runtime-derived strings like stateHistory entries and output tails are sanitized. To ensure no sensitive information (such as local paths or tokens) is leaked, it should be passed through sanitizeRuntimeText.

Suggested change
const producerStatus = displayStatus(producerContext?.status);
const producerStatus = displayStatus(sanitizeRuntimeText(producerContext?.status));

const stdoutTail = sanitizedTail(producerContext?.stdoutTail);
const stderrTail = sanitizedTail(producerContext?.stderrTail);
const stateHistory = producerContext?.stateHistory ?? [];
const hasProducerContext = !!producerContext && (
hasValue(producerStatus)
|| hasValue(producerContext.updatedAt)
|| hasValue(stdoutTail)
|| hasValue(stderrTail)
|| stateHistory.length > 0
);

return (
<div className="space-y-4">
Expand Down Expand Up @@ -249,6 +303,55 @@ export function ReportAuditView({
</div>
<div className="space-y-2 text-[13px] text-muted-foreground">
<p>{historyPlaceholder}</p>
{hasProducerContext ? (
<div className="mt-3 rounded-2xl border border-border bg-background p-3">
<h3 className="text-[13px] font-bold text-foreground">{t.producerTitle}</h3>
<dl className="mt-3 grid gap-3 text-[13px] sm:grid-cols-2">
{producerStatus ? (
<div>
<dt className="font-semibold text-foreground">{t.producerStatus}</dt>
<dd className="mt-1 text-muted-foreground">{producerStatus}</dd>
</div>
) : null}
{producerContext?.updatedAt ? (
<div>
<dt className="font-semibold text-foreground">{t.producerUpdated}</dt>
<dd className="mt-1 text-muted-foreground">{formatFullTime(producerContext.updatedAt, locale)}</dd>
</div>
) : null}
</dl>
{stateHistory.length > 0 ? (
<div className="mt-3">
<div className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{t.stateHistory}
</div>
<div className="mt-2 flex flex-wrap gap-2">
{stateHistory.map((entry, index) => (
<span
key={`${entry.state}-${entry.timestamp ?? "none"}-${index}`}
className="rounded-xl border border-border bg-card px-2 py-1 text-[11px] text-muted-foreground"
>
<span className="font-semibold text-foreground">{displayStatus(sanitizeRuntimeText(entry.state)) ?? "Unknown"}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the localized string from the t object for the unknown state fallback.

Suggested change
<span className="font-semibold text-foreground">{displayStatus(sanitizeRuntimeText(entry.state)) ?? "Unknown"}</span>
<span className="font-semibold text-foreground">{displayStatus(sanitizeRuntimeText(entry.state)) ?? t.unknownState}</span>

{entry.timestamp ? (
<span className="ml-1 mono">{formatFullTime(entry.timestamp, locale)}</span>
) : null}
</span>
))}
</div>
</div>
) : null}
{stdoutTail || stderrTail ? (
<div className="mt-3">
<div className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{t.outputTail}
</div>
<pre className="mt-2 max-h-44 overflow-y-auto whitespace-pre-wrap break-all rounded-xl border border-border bg-card p-3 mono text-[11px] leading-relaxed text-muted-foreground">
{[stdoutTail, stderrTail].filter(Boolean).join("\n")}
</pre>
</div>
) : null}
</div>
) : null}
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,51 @@ describe("TrackReportPage", () => {
expect(markup).toContain("recon_artifact_mainline");
});

it("hydrates sanitized producer context from the completed job detail facade", async () => {
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.includes("/api/v1/audit/jobs/job_demo_004")) {
return Response.json({
job: {
status: "completed",
updated_at: "2026-05-15T08:12:30Z",
stdout_tail: "saved C:\\runtime\\private\\score.json\nreported token=abc123",
stderr_tail: "upload to http://localhost:8780 failed",
state_history: [
{ state: "queued", timestamp: "2026-05-15T08:12:00Z" },
{ state: "completed", timestamp: "2026-05-15T08:12:30Z" },
],
},
});
}

return new Response(null, { status: 404 });
}));

const markup = renderToStaticMarkup(
await renderTrackReportPage({
locale: "en-US",
params: { track: "black-box" },
searchParams: {
view: "audit",
job: "job_demo_004",
contract: "recon_artifact_mainline",
model: "stable-diffusion-v1-4",
auc: "0.849",
},
}),
);

expect(markup).toContain("Producer context");
expect(markup).toContain("Runtime output tail");
expect(markup).toContain("&lt;local-path&gt;");
expect(markup).toContain("token=&lt;redacted&gt;");
expect(markup).toContain("&lt;runtime-url&gt;");
expect(markup).not.toContain("C:\\runtime\\private");
expect(markup).not.toContain("abc123");
expect(markup).not.toContain("localhost:8780");
});

it("keeps completed job context safe when no snapshot row matches", async () => {
const markup = renderToStaticMarkup(
await renderTrackReportPage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { type EvidenceSummaryPayload } from "@/lib/audit-client";
import { backendBaseUrl } from "@/lib/api-proxy";
import { fetchWithTimeout } from "@/lib/fetch-timeout";
import { resolveLocaleFromHeaderStore } from "@/lib/locale";
import { WORKSPACE_COPY } from "@/lib/workspace-copy";
import { ReportAuditView, type ReportProvenance } from "./ReportAuditView";
import { ReportAuditView, type ReportProducerContext, type ReportProvenance } from "./ReportAuditView";
import { ReportDisplayView } from "./ReportDisplayView";

const DEFAULT_SERVER_FETCH_TIMEOUT_MS = 600;
Expand Down Expand Up @@ -94,20 +93,14 @@ function pageTitle(locale: Locale, label: string) {
return locale === "zh-CN" ? `${label}报告详情` : `${label} report details`;
}

function pageDescription(locale: Locale, label: string) {
return locale === "zh-CN"
? "同一审计线路的展示视图与长期审计视图共用这一个入口,默认保持展示形态。"
: `This route keeps both the display view and the long-term audit view for the ${label.toLowerCase()} track.`;
}

function toggleLabel(locale: Locale, view: ViewMode) {
if (locale === "zh-CN") {
return view === "display" ? "展示视图" : "审计视图";
}
return view === "display" ? "Display view" : "Audit view";
}

function historyPlaceholder(locale: Locale, label: string) {
function historyPlaceholder(locale: Locale) {
return locale === "zh-CN"
? "该审计线路的历史对比功能将在后续版本接入,当前为占位区域。"
: "Historical comparison for this track will be available in a future release. This area is reserved.";
Expand Down Expand Up @@ -204,6 +197,73 @@ function buildJobContext(searchParams?: TrackReportPageSearchParams) {
};
}

function readJobEnvelope(payload: unknown) {
if (!payload || typeof payload !== "object") {
return null;
}

const record = payload as Record<string, unknown>;
const job = record.job;
if (job && typeof job === "object") {
return job as Record<string, unknown>;
}
return record;
}

function readStateHistory(value: unknown): ReportProducerContext["stateHistory"] {
if (!Array.isArray(value)) {
return [];
}

return value.flatMap((entry) => {
if (!entry || typeof entry !== "object") {
return [];
}

const record = entry as Record<string, unknown>;
if (typeof record.state !== "string") {
return [];
}

return [{
state: record.state,
timestamp: typeof record.timestamp === "string" ? record.timestamp : null,
}];
});
}

async function fetchProducerContext(jobContext: ReturnType<typeof buildJobContext>): Promise<ReportProducerContext | undefined> {
if (!jobContext?.jobId) {
return undefined;
}

try {
const response = await fetchWithTimeout(
new URL(`/api/v1/audit/jobs/${encodeURIComponent(jobContext.jobId)}`, backendBaseUrl()),
{ cache: "no-store" },
{ timeoutMs: DEFAULT_SERVER_FETCH_TIMEOUT_MS },
);
if (!response.ok) {
return undefined;
}

const job = readJobEnvelope(await response.json());
if (!job) {
return undefined;
}

return {
status: typeof job.status === "string" ? job.status : null,
updatedAt: typeof job.updated_at === "string" ? job.updated_at : null,
stdoutTail: typeof job.stdout_tail === "string" ? job.stdout_tail : null,
stderrTail: typeof job.stderr_tail === "string" ? job.stderr_tail : null,
stateHistory: readStateHistory(job.state_history),
};
} catch {
return undefined;
}
}

function findHighlightedRows(rows: AttackDefenseRowViewModel[], jobContext: ReturnType<typeof buildJobContext>) {
if (!jobContext) {
return [];
Expand Down Expand Up @@ -243,9 +303,11 @@ export async function renderTrackReportPage({
const rows = filterRows(table?.rows ?? [], track);
const highlightedRowKeys = findHighlightedRows(rows, jobContext);
const catalogEntries = catalog?.tracks.find((item) => item.track === track)?.entries ?? [];
const provenance = await fetchTrackProvenance(pickPrimaryEntry(catalogEntries));
const [provenance, producerContext] = await Promise.all([
fetchTrackProvenance(pickPrimaryEntry(catalogEntries)),
fetchProducerContext(jobContext),
]);
const label = trackLabel(resolvedLocale, track);
const copy = WORKSPACE_COPY[resolvedLocale].reports;
const displayHref = `/workspace/reports/${track}?view=display`;
const auditHref = `/workspace/reports/${track}?view=audit`;

Expand Down Expand Up @@ -290,8 +352,9 @@ export async function renderTrackReportPage({
locale={resolvedLocale}
rows={rows}
provenance={provenance}
historyPlaceholder={historyPlaceholder(resolvedLocale, label)}
historyPlaceholder={historyPlaceholder(resolvedLocale)}
jobContext={jobContext}
producerContext={producerContext}
highlightedRowKeys={highlightedRowKeys}
/>
) : (
Expand Down
Loading
Loading