mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
Advance TS port Effect workbench
This commit is contained in:
parent
92dae8c374
commit
3515106670
116 changed files with 12286 additions and 9584 deletions
49
ts/packages/workbench/tests/workbench-qa/chat-graph.spec.ts
Normal file
49
ts/packages/workbench/tests/workbench-qa/chat-graph.spec.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { expect, expectCanvasNonBlank, expectHeading, gotoWorkbench, test } from "./fixtures";
|
||||
|
||||
test("chat streams, cancels, regenerates, copies, and clears messages", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/chat", { chat: { delayFrames: 6 } });
|
||||
await expectHeading(page, "Chat");
|
||||
|
||||
await page.getByPlaceholder(/Ask using Graph RAG/).fill("cancel this request");
|
||||
await page.getByLabel("Send message").click();
|
||||
await expect(page.getByLabel("Cancel request")).toBeVisible();
|
||||
await page.getByLabel("Cancel request").click();
|
||||
await expect(page.getByText("(Cancelled)")).toBeVisible();
|
||||
|
||||
await page.getByLabel("Clear conversation").click();
|
||||
await expect(page.getByText("Start a conversation with TrustGraph.")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder(/Ask using Graph RAG/).fill("Who does Alice know?");
|
||||
await page.getByLabel("Send message").click();
|
||||
await expect(page.getByText(/Mock graph answer from QA knowledge graph/)).toBeVisible();
|
||||
await expect(page.getByText("qa-model")).toBeVisible();
|
||||
|
||||
await page.getByText(/Mock graph answer/).hover();
|
||||
await page.getByLabel("Copy message").click();
|
||||
await expect.poll(() => page.evaluate(() => (window as Window & { __TRUSTGRAPH_WORKBENCH_CLIPBOARD__?: string }).__TRUSTGRAPH_WORKBENCH_CLIPBOARD__)).toContain("Mock graph answer");
|
||||
|
||||
await page.getByLabel("Regenerate response").click();
|
||||
await expect(page.getByText(/Mock graph answer from QA knowledge graph/).last()).toBeVisible();
|
||||
|
||||
await page.getByText("Agent").click();
|
||||
await page.getByPlaceholder(/Ask using Agent/).fill("Use the agent");
|
||||
await page.getByLabel("Send message").click();
|
||||
await expect(page.getByText("Thinking")).toBeVisible();
|
||||
await expect(page.getByText("Observing")).toBeVisible();
|
||||
await expect(page.getByText("Mock agent answer from TrustGraph.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("graph renders seeded triples and responds to controls", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/graph");
|
||||
await expectHeading(page, "Graph");
|
||||
await expect(page.getByText(/nodes/).first()).toBeVisible();
|
||||
await expect(page.getByText(/edges/).first()).toBeVisible();
|
||||
|
||||
await expectCanvasNonBlank(page.locator("canvas").first());
|
||||
|
||||
await page.getByPlaceholder("Search graph...").fill("Alice");
|
||||
await expect(page.getByText(/1 nodes/)).toBeVisible();
|
||||
await page.getByRole("button", { name: "Labels" }).click();
|
||||
await page.getByLabel("Node limit").selectOption("100");
|
||||
await expect(page.getByText(/nodes/).first()).toBeVisible();
|
||||
});
|
||||
88
ts/packages/workbench/tests/workbench-qa/fixtures.ts
Normal file
88
ts/packages/workbench/tests/workbench-qa/fixtures.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { expect, test as base, type Locator, type Page } from "@playwright/test";
|
||||
import type { MockWorkbenchFixture } from "../../src/qa/mock-api";
|
||||
|
||||
const consoleAllowlist = [
|
||||
"Download the React DevTools",
|
||||
'apple-mobile-web-app-capable',
|
||||
];
|
||||
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use) => {
|
||||
const errors: string[] = [];
|
||||
page.on("console", (message) => {
|
||||
const text = message.text();
|
||||
if (message.type() === "error" && !consoleAllowlist.some((allowed) => text.includes(allowed))) {
|
||||
errors.push(text);
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
errors.push(error.message);
|
||||
});
|
||||
await use(page);
|
||||
expect(errors, "unexpected browser console/page errors").toEqual([]);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
|
||||
export function fixture(overrides: MockWorkbenchFixture = {}): MockWorkbenchFixture {
|
||||
return {
|
||||
...overrides,
|
||||
settings: {
|
||||
...(overrides.settings ?? {}),
|
||||
featureSwitches: {
|
||||
mcpTools: true,
|
||||
...(overrides.settings?.featureSwitches ?? {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareWorkbench(page: Page, overrides: MockWorkbenchFixture = {}) {
|
||||
await page.addInitScript((input) => {
|
||||
const qaWindow = window as Window & {
|
||||
__TRUSTGRAPH_WORKBENCH_QA__?: unknown;
|
||||
__TRUSTGRAPH_WORKBENCH_CLIPBOARD__?: string;
|
||||
};
|
||||
window.localStorage.clear();
|
||||
qaWindow.__TRUSTGRAPH_WORKBENCH_QA__ = {
|
||||
enabled: true,
|
||||
fixture: input,
|
||||
flowId: "default",
|
||||
};
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: {
|
||||
writeText: (text: string) => {
|
||||
qaWindow.__TRUSTGRAPH_WORKBENCH_CLIPBOARD__ = text;
|
||||
return Promise.resolve();
|
||||
},
|
||||
readText: () => Promise.resolve(qaWindow.__TRUSTGRAPH_WORKBENCH_CLIPBOARD__ ?? ""),
|
||||
},
|
||||
});
|
||||
}, fixture(overrides));
|
||||
}
|
||||
|
||||
export async function gotoWorkbench(page: Page, path: string, overrides: MockWorkbenchFixture = {}) {
|
||||
await prepareWorkbench(page, overrides);
|
||||
await page.goto(path);
|
||||
await expect(page.locator("body")).not.toContainText("Something went wrong");
|
||||
}
|
||||
|
||||
export async function expectHeading(page: Page, name: string) {
|
||||
await expect(page.getByRole("heading", { name, level: 1 })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectCanvasNonBlank(canvas: Locator) {
|
||||
await expect(canvas).toBeVisible();
|
||||
await expect.poll(() => canvas.evaluate((node) => {
|
||||
if (!(node instanceof HTMLCanvasElement)) return false;
|
||||
const context = node.getContext("2d");
|
||||
if (context === null || node.width === 0 || node.height === 0) return false;
|
||||
const pixels = context.getImageData(0, 0, node.width, node.height).data;
|
||||
for (let index = 3; index < pixels.length; index += 16) {
|
||||
if (pixels[index] !== 0) return true;
|
||||
}
|
||||
return false;
|
||||
}), { timeout: 10_000 }).toBe(true);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { expect, expectHeading, gotoWorkbench, test } from "./fixtures";
|
||||
|
||||
test("flows validate parameters, start, expand, and stop", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/flows");
|
||||
await expectHeading(page, "Flows");
|
||||
await expect(page.getByRole("button", { name: /qa-flow Seeded QA flow/ })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Start Flow" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Start Flow" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Start" }).click();
|
||||
await expect(page.getByText("Flow ID is required")).toBeVisible();
|
||||
|
||||
await dialog.getByPlaceholder("my-flow-id").fill("qa-started");
|
||||
await dialog.locator("select").selectOption("qa-blueprint");
|
||||
await dialog.getByPlaceholder("What this flow does").fill("Started from browser QA");
|
||||
await dialog.getByText("Blueprint Details").click();
|
||||
await expect(dialog.locator("pre")).toContainText("qa-blueprint");
|
||||
await dialog.locator("textarea").fill("{bad");
|
||||
await dialog.getByRole("button", { name: "Start" }).click();
|
||||
await expect(page.getByText("Invalid JSON")).toBeVisible();
|
||||
|
||||
await dialog.locator("textarea").fill('{"temperature":0.2}');
|
||||
await dialog.getByRole("button", { name: "Start" }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
const startedFlow = page.getByRole("button", { name: /qa-started Started from browser QA/ });
|
||||
await expect(startedFlow).toBeVisible();
|
||||
|
||||
await startedFlow.click();
|
||||
await expect(page.locator("pre").last()).toContainText("Started from browser QA");
|
||||
await page.getByLabel("Stop flow qa-started").click();
|
||||
await expect(startedFlow).toBeHidden();
|
||||
});
|
||||
|
||||
test("knowledge cores load and delete with confirmation", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/knowledge-cores");
|
||||
await expectHeading(page, "Knowledge Cores");
|
||||
await expect(page.getByRole("row", { name: /qa-core/ })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Load core qa-core").click();
|
||||
await expect(page.getByText("Core loaded")).toBeVisible();
|
||||
|
||||
await page.getByLabel("Delete core qa-core").click();
|
||||
const dialog = page.getByRole("dialog", { name: "Delete Knowledge Core" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
|
||||
await page.getByLabel("Delete core qa-core").click();
|
||||
await dialog.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(page.getByRole("row", { name: /qa-core/ })).toBeHidden();
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { expect, expectHeading, gotoWorkbench, test } from "./fixtures";
|
||||
|
||||
const routes = [
|
||||
["/chat", "Chat"],
|
||||
["/library", "Library"],
|
||||
["/graph", "Graph"],
|
||||
["/prompts", "Prompts"],
|
||||
["/token-cost", "Token Cost"],
|
||||
["/knowledge-cores", "Knowledge Cores"],
|
||||
["/flows", "Flows"],
|
||||
["/mcp-tools", "MCP Tools"],
|
||||
["/settings", "Settings"],
|
||||
] as const;
|
||||
|
||||
for (const [route, heading] of routes) {
|
||||
test(`loads ${route}`, async ({ page }) => {
|
||||
await gotoWorkbench(page, route);
|
||||
await expectHeading(page, heading);
|
||||
await expect(page.getByRole("navigation", { name: "Main navigation" })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test("settings persist view state and drive feature switches", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/settings");
|
||||
await expectHeading(page, "Settings");
|
||||
|
||||
const apiKey = page.getByPlaceholder("Optional gateway bearer token");
|
||||
await apiKey.fill("qa-secret");
|
||||
await expect(apiKey).toHaveAttribute("type", "password");
|
||||
await page.getByLabel("Show API key").click();
|
||||
await expect(apiKey).toHaveAttribute("type", "text");
|
||||
await page.getByLabel("Hide API key").click();
|
||||
await expect(apiKey).toHaveAttribute("type", "password");
|
||||
|
||||
await page.getByText("Theme").click();
|
||||
await expect.poll(() => page.evaluate(() => document.body.classList.contains("light"))).toBe(true);
|
||||
|
||||
await page.getByLabel("Create collection").click();
|
||||
const collectionDialog = page.getByRole("dialog", { name: "Create Collection" });
|
||||
await expect(collectionDialog).toBeVisible();
|
||||
await collectionDialog.getByRole("textbox", { name: "Collection ID" }).fill("qa-created");
|
||||
await collectionDialog.getByRole("textbox", { name: "Display Name" }).fill("QA Created");
|
||||
await collectionDialog.getByRole("button", { name: "Create", exact: true }).click();
|
||||
await expect(page.getByRole("option", { name: "qa-created" })).toBeAttached();
|
||||
|
||||
const mcpToggle = page.locator("label").filter({ hasText: "MCP Tools" }).getByRole("checkbox");
|
||||
await mcpToggle.uncheck();
|
||||
await expect(page.getByRole("navigation", { name: "Main navigation" }).getByRole("link", { name: "MCP Tools" })).toBeHidden();
|
||||
await mcpToggle.check();
|
||||
await expect(page.getByRole("navigation", { name: "Main navigation" }).getByRole("link", { name: "MCP Tools" })).toBeVisible();
|
||||
});
|
||||
44
ts/packages/workbench/tests/workbench-qa/library.spec.ts
Normal file
44
ts/packages/workbench/tests/workbench-qa/library.spec.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { writeFileSync } from "node:fs";
|
||||
import { expect, expectHeading, gotoWorkbench, test } from "./fixtures";
|
||||
|
||||
test("library searches, shows metadata, uploads small and chunked files, and deletes", async ({ page }, testInfo) => {
|
||||
await gotoWorkbench(page, "/library");
|
||||
await expectHeading(page, "Library");
|
||||
await expect(page.getByText("QA Document")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder("Search documents...").fill("missing");
|
||||
await expect(page.getByText("No documents match your search.")).toBeVisible();
|
||||
await page.getByPlaceholder("Search documents...").fill("QA");
|
||||
await expect(page.getByText("QA Document")).toBeVisible();
|
||||
|
||||
await page.getByLabel("View QA Document").click();
|
||||
await expect(page.getByRole("dialog", { name: "Document Details" })).toBeVisible();
|
||||
await expect(page.getByText("Seeded document for browser QA")).toBeVisible();
|
||||
await page.getByLabel("Close dialog").click();
|
||||
|
||||
const smallFile = testInfo.outputPath("qa-upload-small.txt");
|
||||
writeFileSync(smallFile, "Small QA upload");
|
||||
await page.getByRole("button", { name: "Upload", exact: true }).click();
|
||||
const uploadDialog = page.getByRole("dialog", { name: "Upload Document" });
|
||||
await expect(uploadDialog).toBeVisible();
|
||||
await page.locator('input[type="file"]').setInputFiles(smallFile);
|
||||
await expect(uploadDialog.getByRole("textbox", { name: "Title" })).toHaveValue("qa-upload-small");
|
||||
await uploadDialog.getByRole("button", { name: "Upload" }).click();
|
||||
await expect(uploadDialog).toBeHidden();
|
||||
await expect(page.getByRole("row", { name: /qa-upload-small/ })).toBeVisible();
|
||||
|
||||
const chunkedFile = testInfo.outputPath("qa-upload-chunked.txt");
|
||||
writeFileSync(chunkedFile, "chunked ".repeat(180_000));
|
||||
await page.getByRole("button", { name: "Upload", exact: true }).click();
|
||||
const chunkedDialog = page.getByRole("dialog", { name: "Upload Document" });
|
||||
await page.locator('input[type="file"]').setInputFiles(chunkedFile);
|
||||
await expect(chunkedDialog.getByRole("textbox", { name: "Title" })).toHaveValue("qa-upload-chunked");
|
||||
await chunkedDialog.getByRole("button", { name: "Upload" }).click();
|
||||
await expect(page.getByRole("row", { name: /qa-upload-chunked/ })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Delete qa-upload-small").click();
|
||||
const deleteDialog = page.getByRole("dialog", { name: "Delete Document" });
|
||||
await expect(deleteDialog).toBeVisible();
|
||||
await deleteDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
await expect(page.getByRole("row", { name: /qa-upload-small/ })).toBeHidden();
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { expect, expectHeading, gotoWorkbench, test } from "./fixtures";
|
||||
|
||||
test("prompts and token costs render seeded config", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/prompts");
|
||||
await expectHeading(page, "Prompts");
|
||||
await expect(page.getByRole("tab", { name: "Templates" })).toHaveAttribute("aria-selected", "true");
|
||||
await page.getByRole("button", { name: "qa-template" }).click();
|
||||
await expect(page.getByText("QA template system")).toBeVisible();
|
||||
await expect(page.getByText("Answer the QA question")).toBeVisible();
|
||||
await page.getByLabel("Close prompt detail").click();
|
||||
await page.getByRole("tab", { name: "System Prompt" }).click();
|
||||
await expect(page.getByText("You are the QA system prompt.")).toBeVisible();
|
||||
|
||||
await page.goto("/token-cost");
|
||||
await expectHeading(page, "Token Cost");
|
||||
await expect(page.getByText("qa-model")).toBeVisible();
|
||||
await expect(page.getByText("$1.25")).toBeVisible();
|
||||
await expect(page.getByText("$2.50")).toBeVisible();
|
||||
});
|
||||
|
||||
test("mcp tools add, validate, edit, and delete servers and tools", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/mcp-tools");
|
||||
await expectHeading(page, "MCP Tools");
|
||||
const main = page.locator("#main-content");
|
||||
await expect(main.getByText("qa-search")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Add Server" }).click();
|
||||
let dialog = page.getByRole("dialog", { name: "Add MCP Server" });
|
||||
await dialog.getByLabel("Key").fill("qa-extra-server");
|
||||
await dialog.getByLabel("URL").fill("http://localhost:8383/mcp-extra");
|
||||
await dialog.getByLabel("Remote Name").fill("qa-extra");
|
||||
const authToken = dialog.getByRole("textbox", { name: /Auth Token/ });
|
||||
await authToken.fill("token");
|
||||
await dialog.getByLabel("Show auth token").click();
|
||||
await expect(authToken).toHaveAttribute("type", "text");
|
||||
await dialog.getByRole("button", { name: "Save" }).click();
|
||||
await expect(main.getByText("qa-extra-server")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Add Server" }).click();
|
||||
dialog = page.getByRole("dialog", { name: "Add MCP Server" });
|
||||
await dialog.getByLabel("Key").fill("qa-search");
|
||||
await dialog.getByLabel("URL").fill("http://localhost:8383/duplicate");
|
||||
await dialog.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByText("A server with this key already exists")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
await page.getByRole("button", { name: /Tools/ }).click();
|
||||
await page.getByRole("button", { name: "Add Tool" }).click();
|
||||
dialog = page.getByRole("dialog", { name: "Add MCP Tool" });
|
||||
await dialog.getByLabel("Key").fill("qa-extra-tool");
|
||||
await dialog.getByLabel("Name").fill("QA Extra Tool");
|
||||
await dialog.getByLabel("Description").fill("Extra tool from browser QA");
|
||||
await dialog.getByLabel("MCP Server").selectOption("qa-extra-server");
|
||||
await dialog.getByLabel("Groups").fill("default, qa");
|
||||
await dialog.getByRole("button", { name: "Add" }).click();
|
||||
await dialog.getByPlaceholder("name").fill("query");
|
||||
await dialog.getByPlaceholder("description").fill("Query to execute");
|
||||
await dialog.getByRole("button", { name: "Save" }).click();
|
||||
await expect(main.getByText("qa-extra-tool")).toBeVisible();
|
||||
|
||||
await page.getByLabel("Edit tool qa-extra-tool").click();
|
||||
dialog = page.getByRole("dialog", { name: "Edit MCP Tool" });
|
||||
await dialog.getByLabel("Name").fill("QA Extra Tool Edited");
|
||||
await dialog.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByText("QA Extra Tool Edited")).toBeVisible();
|
||||
|
||||
await page.getByLabel("Delete tool qa-extra-tool").click();
|
||||
dialog = page.getByRole("dialog", { name: "Delete tool" });
|
||||
await dialog.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(main.getByText("qa-extra-tool")).toBeHidden();
|
||||
|
||||
await page.getByRole("button", { name: /Servers/ }).click();
|
||||
await page.getByLabel("Delete server qa-extra-server").click();
|
||||
dialog = page.getByRole("dialog", { name: "Delete server" });
|
||||
await dialog.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(main.getByText("qa-extra-server")).toBeHidden();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue