112 lines
5.7 KiB
TypeScript
112 lines
5.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
calculateVirtualGridColumnCount,
|
|
calculateVirtualGridRowWindowLimit,
|
|
chunkVirtualGridItems,
|
|
} from "../src/components/common/virtual-grid.ts";
|
|
|
|
const viewSource = readSource("../src/views/agents/AgentRunsView.vue");
|
|
const inspectorSource = readSource("../src/components/agents/AgentRunInspector.vue");
|
|
const inspectorPanelSource = readSource("../src/components/console/InspectorPanel.vue");
|
|
const mindMapSource = readSource("../src/components/agents/AgentRunMindMap.vue");
|
|
const virtualGridSource = readSource("../src/components/common/VirtualGrid.vue");
|
|
const styleSource = readSource("../src/styles/agent-observer.css");
|
|
|
|
test("VirtualGrid calculates three responsive column tiers", () => {
|
|
assert.equal(calculateVirtualGridColumnCount(1920, 360, 3, 10), 3);
|
|
assert.equal(calculateVirtualGridColumnCount(960, 360, 3, 10), 2);
|
|
assert.equal(calculateVirtualGridColumnCount(390, 360, 3, 10), 1);
|
|
});
|
|
|
|
test("VirtualGrid chunks stable rows and bounds the rendered row window", () => {
|
|
const items = Array.from({ length: 10 }, (_, index) => ({ id: `run-${index}` }));
|
|
const rows = chunkVirtualGridItems(items, 3, (item) => item.id);
|
|
assert.deepEqual(rows.map((row) => row.items.length), [3, 3, 3, 1]);
|
|
assert.deepEqual(rows.map((row) => row.startIndex), [0, 3, 6, 9]);
|
|
assert.equal(rows[0]?.key, "0:run-0|run-1|run-2");
|
|
assert.equal(calculateVirtualGridRowWindowLimit(12, 3), 4);
|
|
assert.equal(calculateVirtualGridRowWindowLimit(12, 1), 12);
|
|
assert.match(virtualGridSource, /ResizeObserver/);
|
|
assert.match(virtualGridSource, /:window-limit="rowWindowLimit"/);
|
|
assert.match(virtualGridSource, /role="listitem"/);
|
|
});
|
|
|
|
test("AgentRun table exposes the eight-column product contract", () => {
|
|
const headers = [
|
|
"状态",
|
|
"Run / Task / Command identity",
|
|
"Project / Workspace",
|
|
"Agent / Profile",
|
|
"阶段",
|
|
"活动 / 耗时",
|
|
"Runner / Node / Lane",
|
|
"结果 / 错误",
|
|
];
|
|
let previousIndex = -1;
|
|
for (const header of headers) {
|
|
const index = viewSource.indexOf(`role="columnheader">${header}`);
|
|
assert.ok(index > previousIndex, `missing or unordered table header: ${header}`);
|
|
previousIndex = index;
|
|
}
|
|
});
|
|
|
|
test("AgentRun page reuses shared status, viewer and embedded inspector contracts", () => {
|
|
assert.match(viewSource, /import StatusStrip/);
|
|
assert.match(viewSource, /<StatusStrip :items="statusItems"/);
|
|
assert.match(viewSource, /import VirtualGrid/);
|
|
assert.match(viewSource, /<VirtualGrid/);
|
|
assert.match(viewSource, /<AgentRunInspector v-if="observer\.selectedRun" embedded/);
|
|
assert.match(inspectorSource, /import ContentViewer/);
|
|
assert.match(inspectorSource, /mode="log"/);
|
|
assert.match(inspectorSource, /mode="json"/);
|
|
assert.doesNotMatch(inspectorSource, /<pre/);
|
|
assert.match(inspectorPanelSource, /embedded\?: boolean/);
|
|
assert.match(inspectorPanelSource, /v-if="!embedded"/);
|
|
});
|
|
|
|
test("AgentRun command bar preserves compact desktop and mobile primary controls", () => {
|
|
assert.match(styleSource, /@media \(max-width: 1100px\)/);
|
|
assert.match(styleSource, /agent-observer-secondary-filters \{ order: 5; flex: 1 0 100%; \}/);
|
|
assert.match(styleSource, /@media \(max-width: 600px\)/);
|
|
assert.match(styleSource, /agent-observer-secondary-filters,[\s\S]*agent-observer-density \{ display: none; \}/);
|
|
assert.match(styleSource, /agent-observer-filter-trigger \{ grid-column: 2; display: inline-flex; \}/);
|
|
assert.match(styleSource, /agent-observer-segment \{ grid-column: 1 \/ -1; width: 100%; \}/);
|
|
assert.match(styleSource, /@media \(prefers-reduced-motion: reduce\)/);
|
|
assert.match(viewSource, /class="agent-observer-search"/);
|
|
assert.match(viewSource, /class="agent-observer-primary-status"/);
|
|
assert.match(viewSource, /class="agent-observer-segment" role="group" aria-label="视图切换"/);
|
|
assert.match(viewSource, /class="agent-observer-stream"/);
|
|
});
|
|
|
|
test("AgentRun workspace always owns the remaining grid row", () => {
|
|
assert.match(styleSource, /agent-observer-page \{[\s\S]*?height: 100%;/);
|
|
assert.match(styleSource, /agent-observer-workspace \{ grid-row: -2 \/ -1;/);
|
|
});
|
|
|
|
test("AgentRun mind map always owns the flexible tree row", () => {
|
|
assert.match(styleSource, /agent-observer-tree > \.agent-mind-map \{ grid-row: -2 \/ -1; \}/);
|
|
});
|
|
|
|
test("AgentRun mind map connects custom nodes through Vue Flow handles", () => {
|
|
assert.match(mindMapSource, /import \{ Handle, MarkerType, Position, VueFlow/);
|
|
assert.match(mindMapSource, /<Handle v-if="data\.row\.parentId" type="target" :position="Position\.Left" \/>/);
|
|
assert.match(mindMapSource, /<Handle v-if="data\.row\.childCount > 0" type="source" :position="Position\.Right" \/>/);
|
|
assert.match(styleSource, /agent-mind-map-flow \.vue-flow__handle/);
|
|
assert.match(styleSource, /agent-mind-map-flow \.vue-flow__edge-path \{[^}]*vector-effect: non-scaling-stroke;/);
|
|
assert.match(styleSource, /agent-mind-map-flow \.vue-flow__edge-path \{[^}]*stroke: var\(--console-graphite-500\);/);
|
|
assert.match(styleSource, /agent-mind-map-flow \.vue-flow__arrowhead \{ fill: var\(--console-graphite-500\); \}/);
|
|
});
|
|
|
|
test("AgentRun inspector keeps chrome fixed and scrolls only its body", () => {
|
|
assert.match(styleSource, /agent-observer-inspector \{[^}]*overflow: hidden;/);
|
|
assert.match(styleSource, /agent-observer-inspector > \.operations-inspector-panel \{[^}]*grid-template-rows: auto auto minmax\(0, 1fr\) auto;/);
|
|
assert.match(styleSource, /agent-observer-inspector \.operations-inspector-body \{[^}]*overflow: auto;/);
|
|
});
|
|
|
|
function readSource(relativePath: string): string {
|
|
return readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
|
}
|