feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s
Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
|
||||
let server: ViteDevServer;
|
||||
let url: string;
|
||||
test.beforeAll(async () => {
|
||||
server = await createServer({ server: { host: "127.0.0.1", port: 0 } });
|
||||
await server.listen();
|
||||
url = server.resolvedUrls!.local[0]! + "e2e/fixtures/archive-repair.html";
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
test("reviewer selects a correction, sees pending activation and retries on desktop/mobile", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", e => errors.push(e.message));
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(url);
|
||||
await expect(page.getByRole("region", { name: "Archive conflict repair" })).toBeVisible();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-x1-repair-desktop.png", fullPage: true });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
await page.screenshot({ path: "/private/tmp/thothii-x1-repair-mobile.png", fullPage: true });
|
||||
await page.getByRole("button", { name: "Apply this correction to Evidence" }).click();
|
||||
await expect(page.getByRole("status").filter({ hasText: "Correction saved" })).toContainText("activation is incomplete");
|
||||
await page.getByRole("button", { name: "Retry selected correction" }).click();
|
||||
await expect(page.getByRole("status").filter({ hasText: "Correction saved" })).toContainText("active in the archive index");
|
||||
const responses = JSON.parse(await page.locator("#response").textContent() || "[]");
|
||||
expect(responses.map((r: { choices: string[] }) => r.choices)).toEqual([["e"], ["e"]]);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Archive repair review</title></head><body><div id="root"></div><script type="module" src="/e2e/fixtures/archive-repair.tsx"></script></body></html>
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ArchiveRepairWidget } from "../../src/widgets/ArchiveRepairWidget";
|
||||
import "../../src/index.css";
|
||||
|
||||
function Preview() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [responses, setResponses] = useState<unknown[]>([]);
|
||||
return <main className="mx-auto max-w-4xl p-4"><ArchiveRepairWidget key={step} descriptor={{
|
||||
id: `repair-${step}`, widget: "archive-repair", reserved: [], repair: {
|
||||
repair_id: "fixture", reason: "Memory defines an order by its number alone. Evidence requires the financial year too. Choose which shared definition to correct.",
|
||||
can_apply: true, choice: step ? "e" : null, saved: step > 0, indexed: step > 1,
|
||||
status: ["proposed", "pending_activation", "active"][Math.min(step, 2)], options: [
|
||||
{ id: "m", label: "Include the financial year in Memory", archive: "memory", target_id: "mem-order-key",
|
||||
before: { subject: "Order identity", detail: "Use the order number.", scope: "Sales" },
|
||||
content: { subject: "Order identity", detail: "Use order number and financial year.", scope: "Sales" } },
|
||||
{ id: "e", label: "Correct Evidence to use the order number", archive: "evidence", target_id: "evidence:order-key",
|
||||
before: { title: "Order key", payload: { rule: "Use order number and financial year." } },
|
||||
content: { title: "Order key", payload: { rule: "Use the order number." },
|
||||
provenance: { kind: "manual", declared_by: "Sales curator" } } },
|
||||
],
|
||||
},
|
||||
}} onRespond={response => { setResponses([...responses, response]); setStep(step + 1); }} />
|
||||
<output id="response" className="sr-only">{JSON.stringify(responses)}</output></main>;
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(<Preview />);
|
||||
@@ -85,7 +85,7 @@ dwh:
|
||||
supported_transports: [postgres_direct]
|
||||
`;
|
||||
|
||||
async function prepareF1Workspace(root) {
|
||||
async function prepareF1Workspace(root, memoryOnly = false) {
|
||||
const source = join(root, "workspace-source");
|
||||
const remote = join(root, "workspace-remote.git");
|
||||
secureDirectory(source);
|
||||
@@ -97,7 +97,8 @@ async function prepareF1Workspace(root) {
|
||||
" name: Fixture workspace",
|
||||
"",
|
||||
].join("\n"));
|
||||
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"), F1_WORKSPACE_DESCRIPTOR);
|
||||
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"),
|
||||
memoryOnly ? F1_WORKSPACE_DESCRIPTOR.split("dwh:")[0] : F1_WORKSPACE_DESCRIPTOR);
|
||||
|
||||
await runFixtureCommand("git", ["init", "--bare", "--initial-branch=main", remote], root);
|
||||
chmodSync(remote, 0o700);
|
||||
@@ -332,7 +333,7 @@ function cleanBackendEnvironment(overrides) {
|
||||
return { ...env, ...overrides };
|
||||
}
|
||||
|
||||
export async function createAuthenticationStack({ withF1Workspace = false } = {}) {
|
||||
export async function createAuthenticationStack({ withF1Workspace = false, memoryRuntime = {} } = {}) {
|
||||
const credentials = resolveFixtureCredentials();
|
||||
const root = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-e2e-"));
|
||||
secureDirectory(root);
|
||||
@@ -363,7 +364,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
const [frontendPort, backendPort] = await Promise.all([freeLoopbackPort(), freeLoopbackPort()]);
|
||||
const publicUrl = `http://127.0.0.1:${frontendPort}`;
|
||||
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
||||
const workspace = withF1Workspace ? await prepareF1Workspace(root) : undefined;
|
||||
const workspace = withF1Workspace ? await prepareF1Workspace(root, Object.keys(memoryRuntime).length > 0) : undefined;
|
||||
const provider = await startFakeOidcProvider({
|
||||
directory: providerRoot,
|
||||
registration: {
|
||||
@@ -515,6 +516,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
|
||||
} : {}),
|
||||
...memoryRuntime,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -535,6 +537,10 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
|
||||
return {
|
||||
publicUrl,
|
||||
fixtureWorkspaceRoot() {
|
||||
if (!workspace) throw safeError("e2e_workspace_fixture_missing");
|
||||
return join(registryRoot, "repo", workspace.id);
|
||||
},
|
||||
localAccount(account) {
|
||||
const found = accounts[account];
|
||||
if (!found) throw safeError("e2e_local_account_unknown");
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
// So we skip the first two args after node (-c <configPath>).
|
||||
|
||||
const argv = process.argv.slice(2); // drop "node" + script path
|
||||
// Archive integration tests run the actual harness through the production runner.
|
||||
// Unrelated session/LLM operations remain deterministic fixtures.
|
||||
if ((argv[0] === "memory" || argv[0] === "evidence" && argv[1] === "admin") && process.env.THT_E2E_MEMORY_BIN) {
|
||||
const { spawnSync } = await import("node:child_process");
|
||||
const result = spawnSync(process.env.THT_E2E_MEMORY_BIN, argv, {
|
||||
env: process.env, stdio: ["ignore", "inherit", "inherit", 3],
|
||||
});
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
// Strip leading "-c <value>" pair (ThtRunner always prepends this).
|
||||
let args = argv;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
|
||||
<body><div id="root"></div><script type="module" src="./memory-review.tsx"></script></body></html>
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { MemoryReviewWidget } from "../../src/widgets/MemoryReviewWidget";
|
||||
import "../../src/index.css";
|
||||
|
||||
const card = { family: "sql_rule", subject: "Join orders within the same financial year",
|
||||
detail: "Join order lines using both order_id and financial_year. The order number repeats across years.",
|
||||
scope: "Sales reporting", rationale: "Confirmed during review of annual totals.",
|
||||
question: "What is the total value of shipped orders?", sql: "", concepts: ["Order grain"],
|
||||
dependencies: [{ database: "warehouse", schema_name: "sales", table: "orders", column: "financial_year" }],
|
||||
links: [] };
|
||||
const descriptor = { id: "review", widget: "memory-review", reserved: [],
|
||||
summary: { summary_id: "fixture", items: [
|
||||
{ id: "rule", card, reason: "Preserve the approved join correction.", source_seqs: [12], before: null, target_id: null },
|
||||
{ id: "solved", card: { ...card, family: "solved_question", subject: "Shipped orders by year",
|
||||
sql: "SELECT financial_year, SUM(total) FROM sales.orders GROUP BY financial_year" },
|
||||
reason: "Keep the approved solution as an example.", source_seqs: [18], before: null, target_id: null },
|
||||
] } };
|
||||
createRoot(document.getElementById("root")!).render(<main className="mx-auto max-w-3xl p-4">
|
||||
<MemoryReviewWidget descriptor={descriptor} onRespond={response => {
|
||||
document.querySelector("#response")!.textContent = JSON.stringify(response);
|
||||
}} /><output id="response" /></main>);
|
||||
@@ -0,0 +1,195 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { resolve } from "node:path";
|
||||
import { createAuthenticationStack } from "./fixtures/auth-stack.mjs";
|
||||
|
||||
// Opt in: real isolated PostgreSQL/Qdrant containers and the Python harness are required.
|
||||
// Auth, browser, Fastify, ThtRunner and Memory persistence are production code.
|
||||
// Embeddings are deterministic; unrelated sessions and Pi use the existing fixture.
|
||||
test.skip(process.env.THT_MEMORY_BROWSER_E2E !== "1", "Set THT_MEMORY_BROWSER_E2E=1");
|
||||
test.setTimeout(180_000);
|
||||
test.use({ actionTimeout: 15_000, viewport: { width: 1440, height: 1000 } });
|
||||
let stack: Awaited<ReturnType<typeof createAuthenticationStack>>;
|
||||
let embedding: Server;
|
||||
const containers: string[] = [];
|
||||
const root = resolve("..");
|
||||
let databaseUrl: string;
|
||||
let qdrantUrl: string;
|
||||
|
||||
function command(bin: string, args: string[], env = process.env) {
|
||||
return execFileSync(bin, args, { cwd: root, env, encoding: "utf8", timeout: 60_000 }).trim();
|
||||
}
|
||||
function container(image: string, port: number, args: string[] = []) {
|
||||
const id = command("docker", ["run", "-d", "--rm", "-p", `127.0.0.1::${port}`, ...args, image]);
|
||||
containers.push(id);
|
||||
return { id, port: Number(command("docker", ["port", id, `${port}/tcp`]).split(":").at(-1)) };
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const pg = container("postgres:16-alpine", 5432, ["-e", "POSTGRES_PASSWORD=memory-test"]);
|
||||
const qdrant = container("qdrant/qdrant:v1.18.2", 6333);
|
||||
qdrantUrl = `http://127.0.0.1:${qdrant.port}`;
|
||||
await expect.poll(async () => { try { return (await fetch(`${qdrantUrl}/healthz`)).ok; } catch { return false; } }).toBe(true);
|
||||
await expect.poll(() => {
|
||||
// The image's temporary initialization server accepts sockets before restarting.
|
||||
// TCP readiness identifies the final server and avoids racing that shutdown.
|
||||
try { command("docker", ["exec", pg.id, "pg_isready", "-h", "127.0.0.1", "-U", "postgres"]); return true; } catch { return false; }
|
||||
}).toBe(true);
|
||||
command("docker", ["exec", pg.id, "psql", "-U", "postgres", "-c",
|
||||
"CREATE ROLE thothii_catalog_runtime LOGIN PASSWORD 'memory-runtime-test'; " +
|
||||
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO thothii_catalog_runtime; " +
|
||||
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO thothii_catalog_runtime"]);
|
||||
const migratorUrl = `postgresql://postgres:memory-test@127.0.0.1:${pg.port}/postgres`;
|
||||
databaseUrl = `postgresql://thothii_catalog_runtime:memory-runtime-test@127.0.0.1:${pg.port}/postgres`;
|
||||
command(resolve(root, "harness/.venv/bin/python"), ["-m", "tht.memory.migrate"],
|
||||
{ ...process.env, THT_CATALOG_MIGRATOR_DATABASE_URL: migratorUrl });
|
||||
command(resolve(root, "backend/node_modules/.bin/tsx"), ["backend/src/catalog/migrate.ts"],
|
||||
{ ...process.env, THT_CATALOG_MIGRATOR_DATABASE_URL: migratorUrl });
|
||||
embedding = createServer(async (request, response) => {
|
||||
let body = "";
|
||||
for await (const chunk of request) body += chunk;
|
||||
const payload = JSON.parse(body || "{}");
|
||||
const texts = Array.isArray(payload.input) ? payload.input : [payload.input];
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify({ embeddings: texts.map(() => [1, 0, 0]) }));
|
||||
});
|
||||
await new Promise<void>(done => embedding.listen(0, "127.0.0.1", done));
|
||||
const address = embedding.address();
|
||||
if (!address || typeof address === "string") throw new Error("embedding address missing");
|
||||
stack = await createAuthenticationStack({ withF1Workspace: true, memoryRuntime: {
|
||||
THT_E2E_MEMORY_BIN: resolve(root, "harness/.venv/bin/tht"),
|
||||
THT_CATALOG_RUNTIME_DATABASE_URL: databaseUrl,
|
||||
THT_CATALOG_DATABASE_URL: databaseUrl,
|
||||
THT_INTERNAL_QDRANT_URL: qdrantUrl,
|
||||
THT_INTERNAL_EMBEDDING_URL: `http://127.0.0.1:${address.port}`,
|
||||
THT_INTERNAL_EMBEDDING_DIMENSIONS: "3",
|
||||
} });
|
||||
await stack.useLocalMode();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await stack?.close();
|
||||
if (embedding) await new Promise<void>(done => embedding.close(() => done()));
|
||||
for (const id of containers.reverse()) command("docker", ["rm", "-f", id]);
|
||||
});
|
||||
|
||||
test("admin manages both archives through the real stack without cross-deletion", async ({ page }) => {
|
||||
page.on("pageerror", error => console.error("Memory browser error:", error.message));
|
||||
await page.goto(stack.publicUrl);
|
||||
await page.getByLabel("Username").fill(stack.localAccount("admin").username);
|
||||
await page.getByLabel("Password", { exact: true }).fill(stack.localAccount("admin").password);
|
||||
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
||||
await expect(page.getByTestId("app-shell")).toBeVisible();
|
||||
// Pull only the temporary fixture registry through the authenticated application API.
|
||||
const pulled = await page.evaluate(async () => {
|
||||
const me = await (await fetch("/api/me")).json();
|
||||
const result = await fetch("/api/workspace-registry/pull", { method: "POST",
|
||||
headers: { "x-thothii-csrf": me.csrfToken } });
|
||||
return { status: result.status, body: await result.json() };
|
||||
});
|
||||
expect(pulled.status, JSON.stringify(pulled.body)).toBe(200);
|
||||
// This checkout belongs to the temporary authentication fixture, never the PSD archive.
|
||||
command(resolve(root, "harness/.venv/bin/python"), ["-c", `
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tht.evidence.canonical import CuratedEvidence, dump_curated_markdown
|
||||
from tht.evidence.local_archive import LocalEvidenceArchive
|
||||
root = Path(os.environ["FIXTURE_ARCHIVE_ROOT"])
|
||||
unit = CuratedEvidence.model_validate({"schema_version":4,"id":"evidence:order-key",
|
||||
"kind":"domain","title":"Order key Evidence","language":"en","purposes":["sql_generation"],
|
||||
"payload":{"rule":"Use order_id and financial_year when joining order lines."},
|
||||
"provenance":{"kind":"manual","declared_by":"Acceptance fixture"}})
|
||||
path = root / "evidence/curated/domain/order-key.md"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(dump_curated_markdown(unit))
|
||||
LocalEvidenceArchive(root).initialize()
|
||||
`], { ...process.env, FIXTURE_ARCHIVE_ROOT: stack.fixtureWorkspaceRoot() });
|
||||
async function openMemory() {
|
||||
await page.getByRole("button", { name: "Administration", exact: true }).click();
|
||||
expect((await page.getByRole("button", {
|
||||
name: /^(Database management|Memory management|Evidence management)$/,
|
||||
}).allTextContents()).map(text => text.trim())).toEqual([
|
||||
"Database management", "Memory management", "Evidence management",
|
||||
]);
|
||||
await page.getByRole("button", { name: "Memory management", exact: true }).click();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-m1-memory-opening.png", fullPage: true });
|
||||
const memory = page.getByRole("main", { name: "Memory management" });
|
||||
await expect(memory).toBeVisible();
|
||||
await memory.getByLabel("Workspace", { exact: true }).selectOption("fixture-workspace");
|
||||
await expect(page.getByRole("button", { name: "New card", exact: true })).toBeVisible();
|
||||
}
|
||||
await openMemory();
|
||||
await page.getByRole("button", { name: "New card", exact: true }).click();
|
||||
await page.getByLabel("Title", { exact: true }).fill("Orders approved");
|
||||
await page.getByLabel("Scope", { exact: true }).fill("Sales");
|
||||
await page.getByLabel("Content", { exact: true }).fill("Only shipped orders");
|
||||
await page.getByRole("button", { name: "Save card", exact: true }).click();
|
||||
await expect(page.getByText("Saved and indexed.", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Edit card", exact: true }).click();
|
||||
await page.getByLabel("Content", { exact: true }).fill("Only fully shipped orders");
|
||||
await page.getByRole("button", { name: "Save card", exact: true }).click();
|
||||
await expect(page.getByText("Only fully shipped orders", { exact: true })).toBeVisible();
|
||||
await stack.restartBackend();
|
||||
await page.reload();
|
||||
await openMemory();
|
||||
await page.getByRole("button", { name: /Orders approved Domain clarification/ }).click();
|
||||
await expect(page.getByText("Only fully shipped orders", { exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-m1-memory-desktop.png", fullPage: true });
|
||||
await page.getByRole("button", { name: "Delete card", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Confirm deletion", exact: true }).click();
|
||||
await expect(page.getByText("Card deleted and removed from recall.", { exact: true })).toBeVisible();
|
||||
const recalled = command(resolve(root, "harness/.venv/bin/python"), ["-c", `
|
||||
import json, os
|
||||
from tht.memory.runtime import admin_service
|
||||
from tht.adapters.vector.qdrant import QdrantVectorStore
|
||||
runtime = {"internalQdrantUrl": os.environ["QDRANT_URL"], "internalEmbeddingDimensions": 3}
|
||||
service = admin_service("fixture-workspace", runtime)
|
||||
store = QdrantVectorStore(base_url=runtime["internalQdrantUrl"], workspace_id="fixture-workspace",
|
||||
collections={"reference":"fixture-workspace-reference", "memory":"fixture-workspace-memory"}, expected_dimension=3)
|
||||
class Searcher:
|
||||
def search(self, vector, top_n, kinds, **kwargs): return store.search(["memory"], vector, limit=top_n, kinds=kinds, **kwargs)
|
||||
class Embedder:
|
||||
def embed_query(self, text): return [1,0,0]
|
||||
try: print(json.dumps(service.recall("orders", searcher=Searcher(), embedder=Embedder())))
|
||||
finally: service.close()
|
||||
`], { ...process.env, THT_CATALOG_RUNTIME_DATABASE_URL: databaseUrl, QDRANT_URL: qdrantUrl,
|
||||
THT_PRINCIPAL_ISSUER: "test", THT_PRINCIPAL_SUBJECT: "admin", THT_PRINCIPAL_IS_ADMIN: "true" });
|
||||
expect(JSON.parse(recalled)).toEqual([]);
|
||||
await expect(page.getByRole("button", { name: "New card", exact: true })).toBeEnabled();
|
||||
if (!await page.getByRole("button", { name: "Evidence management", exact: true }).isVisible())
|
||||
await page.getByRole("button", { name: "Administration", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Evidence management", exact: true }).click();
|
||||
const evidence = page.getByRole("main", { name: "Evidence management" });
|
||||
await expect(evidence).toBeVisible();
|
||||
await evidence.getByRole("combobox", { name: /Workspace/ }).selectOption("fixture-workspace");
|
||||
await evidence.getByRole("button", { name: "Order key Evidence", exact: true }).click();
|
||||
await expect(evidence.getByRole("region", { name: "Evidence detail" })).toContainText(
|
||||
"Use order_id and financial_year when joining order lines.");
|
||||
await evidence.getByLabel("Search Evidence", { exact: true }).fill("no matching knowledge");
|
||||
await expect(evidence.getByText(/No Evidence matches these filters/)).toBeVisible();
|
||||
await evidence.getByLabel("Search Evidence", { exact: true }).fill("financial_year");
|
||||
await expect(evidence.getByRole("button", { name: "Order key Evidence", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-acceptance-evidence-desktop.png", fullPage: true });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(page.getByRole("button", { name: "Navigation", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("complementary", { name: "Session navigation" })).toBeHidden();
|
||||
expect((await evidence.boundingBox())!.width).toBeGreaterThanOrEqual(380);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
await page.screenshot({ path: "/private/tmp/thothii-acceptance-evidence-mobile.png", fullPage: true });
|
||||
await page.getByRole("button", { name: "Navigation", exact: true }).click();
|
||||
const navigation = page.getByRole("dialog", { name: "Navigation", exact: true });
|
||||
await expect(navigation.getByRole("button", { name: "Memory management", exact: true })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(navigation).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: "Navigation", exact: true })).toBeFocused();
|
||||
await page.getByRole("button", { name: "Navigation", exact: true }).click();
|
||||
await navigation.getByRole("button", { name: "Memory management", exact: true }).click();
|
||||
await expect(navigation).toBeHidden();
|
||||
const mobileMemory = page.getByRole("main", { name: "Memory management" });
|
||||
await expect(mobileMemory).toBeVisible();
|
||||
expect((await mobileMemory.boundingBox())!.width).toBeGreaterThanOrEqual(380);
|
||||
const unknown = await page.evaluate(async () => Promise.all(["memory", "evidence"].map(async archive =>
|
||||
(await fetch(`/api/workspaces/unregistered-workspace/${archive}`)).status)));
|
||||
expect(unknown).toEqual([404, 404]);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
|
||||
let server: ViteDevServer;
|
||||
let url: string;
|
||||
test.beforeAll(async () => {
|
||||
server = await createServer({ server: { host: "127.0.0.1", port: 0 } });
|
||||
await server.listen();
|
||||
url = server.resolvedUrls!.local[0]! + "e2e/fixtures/memory-review.html";
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
test("reviewer edits the final summary and selects what to save on desktop and mobile", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", e => errors.push(e.message));
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(url);
|
||||
await expect(page.getByRole("form", { name: "Memory summary" })).toBeVisible();
|
||||
await page.getByLabel("rule detail").fill("Join on order number and year, within the sales database.");
|
||||
await page.getByRole("checkbox", { name: /Shipped orders by year/ }).uncheck();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-m3-summary-desktop.png", fullPage: true });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
await page.screenshot({ path: "/private/tmp/thothii-m3-summary-mobile.png", fullPage: true });
|
||||
await page.getByRole("button", { name: "Save selected and finish" }).click();
|
||||
const response = JSON.parse(await page.locator("#response").textContent() || "{}");
|
||||
expect(JSON.parse(response.text).items).toHaveLength(1);
|
||||
expect(JSON.parse(response.text).items[0].card.detail).toContain("within the sales database");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user