fix: bound Workbench trace rendering

This commit is contained in:
root
2026-07-21 01:42:01 +02:00
parent d052809121
commit cce8002c69
2 changed files with 46 additions and 3 deletions
@@ -1,3 +1,4 @@
// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first; PJ2026-010401080313 Workbench实时权威 draft-2026-07-14-p0-pure-kafka-authority.
// @vitest-environment jsdom
import { beforeEach, describe, expect, it } from "vitest";
@@ -270,6 +271,42 @@ describe("TraceTimeline sequence authority", () => {
expect(completedSummary.text()).not.toMatch(/\b(?:ok|fail|run|running)\b|total=|duration=|exit=|/u);
wrapper.unmount();
});
it("bounds renderer input and DOM rows for a long Kafka Trace", async () => {
const events = Array.from({ length: 1_000 }, (_, index) => ({
sourceEventId: `evt_long_${index + 1}`,
runId: "run_long_trace",
sourceSeq: index + 1,
source: "agentrun.kafka",
type: "assistant",
label: "agentrun:assistant:message",
message: `long trace event ${index + 1}`
}));
const wrapper = mount(TraceTimeline, {
props: {
autoExpanded: true,
trace: {
traceId: "trc_long_timeline",
eventSource: "hwlab-kafka-sse",
status: "running",
eventCount: events.length,
events
}
}
});
await nextTick();
const disclosure = wrapper.get(".trace-disclosure");
expect(disclosure.attributes("data-event-count")).toBe("1000");
expect(disclosure.attributes("data-render-input-count")).toBe("240");
expect(disclosure.attributes("data-row-windowed")).toBe("true");
const rows = wrapper.findAll('[data-testid="trace-render-row"]');
expect(rows).toHaveLength(120);
expect(Number(rows[0]?.attributes("data-event-seq"))).toBeGreaterThanOrEqual(881);
expect(rows.at(-1)?.attributes("data-event-seq")).toBe("1000");
expect(wrapper.text()).toContain("共 1000 events");
wrapper.unmount();
});
});
function liveTrace(status: "running" | "completed") {
@@ -1,3 +1,4 @@
<!-- SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first; PJ2026-010401080313 Workbench实时权威 draft-2026-07-14-p0-pure-kafka-authority. Render a bounded, stable Trace tail from the Kafka SSE projection. -->
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import type { RunnerTrace } from "@/types";
@@ -18,13 +19,17 @@ const props = defineProps<{
}>();
const TRACE_RENDER_ROW_WINDOW = 120;
const TRACE_RENDER_EVENT_WINDOW = TRACE_RENDER_ROW_WINDOW * 2;
const listRef = ref<HTMLOListElement | null>(null);
const expanded = ref(false);
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useWorkbenchBottomFollowScroll(listRef, { threshold: 24 });
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
const rawReadableRows = computed(() => expanded.value ? traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], {
const renderEvents = computed(() => events.value.length > TRACE_RENDER_EVENT_WINDOW
? events.value.slice(-TRACE_RENDER_EVENT_WINDOW)
: events.value);
const rawReadableRows = computed(() => expanded.value ? traceDisplayRows(traceRecord(props.trace), renderEvents.value as Record<string, unknown>[], {
formatClock: formatDisplayClock,
finalResponsePlacement: props.finalResponsePlacement,
outerFinalResponseText: props.outerFinalResponseText
@@ -32,6 +37,7 @@ const rawReadableRows = computed(() => expanded.value ? traceDisplayRows(traceRe
const readableRows = computed(() => rawReadableRows.value);
const hiddenRowCount = computed(() => Math.max(0, readableRows.value.length - TRACE_RENDER_ROW_WINDOW));
const renderedRows = computed(() => hiddenRowCount.value > 0 ? readableRows.value.slice(-TRACE_RENDER_ROW_WINDOW) : readableRows.value);
const inputEventWindowed = computed(() => renderEvents.value.length < events.value.length);
const traceSummaryLabel = computed(() => {
const count = eventCount.value;
if (count <= 0) return String(props.trace?.status ?? "").trim() || "pending";
@@ -159,14 +165,14 @@ function firstNonEmptyString(...values: unknown[]): string | null {
<template>
<section v-if="trace" class="trace-timeline" :data-trace-id="trace.traceId || undefined" :data-status="trace.status" :data-projection-health="trace.projection?.projectionHealth || trace.projectionHealth || undefined" :data-following="following ? 'true' : 'false'">
<details class="trace-disclosure" :open="expanded" :data-event-count="eventCount" :data-readable-row-count="readableRows.length" :data-rendered-row-count="renderedRows.length" :data-row-windowed="hiddenRowCount > 0 ? 'true' : 'false'" @toggle="onDisclosureToggle">
<details class="trace-disclosure" :open="expanded" :data-event-count="eventCount" :data-render-input-count="renderEvents.length" :data-readable-row-count="readableRows.length" :data-rendered-row-count="renderedRows.length" :data-row-windowed="hiddenRowCount > 0 || inputEventWindowed ? 'true' : 'false'" @toggle="onDisclosureToggle">
<summary class="trace-disclosure-summary" aria-label="折叠运行记录">
<span class="trace-disclosure-caret" aria-hidden="true" />
<span class="trace-disclosure-label">运行记录</span>
<span class="trace-disclosure-count">{{ traceSummaryLabel }}</span>
</summary>
<div v-if="expanded" class="trace-disclosure-body">
<p v-if="hiddenRowCount > 0" class="trace-window-notice">显示最近 {{ renderedRows.length }} / {{ readableRows.length }} 行</p>
<p v-if="hiddenRowCount > 0 || inputEventWindowed" class="trace-window-notice">显示最近 {{ renderedRows.length }} 行,共 {{ eventCount }} events</p>
<ol v-if="renderedRows.length > 0" ref="listRef" @scroll="onScroll">
<li v-for="(row, index) in renderedRows" :key="rowKey(row, index)" class="trace-render-row" data-testid="trace-render-row" :data-trace-id="trace.traceId || undefined" :data-event-seq="row.seq ?? undefined" :data-event-seq-authority="row.sequenceAuthority" :data-projected-seq="row.sequenceAuthority === 'projected' ? row.seq ?? undefined : undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-tool-state="row.toolState ?? undefined" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="hiddenRowCount + index + 1">
<details v-if="rowIsTool(row)" class="trace-tool-details" :aria-busy="row.toolState === 'running' ? 'true' : undefined">