Files
ThothII/frontend/e2e/memory-real.spec.ts
T
Codex 82e2c91f42
Publish documentation / publish (push) Successful in 1m27s
feat: implement memory and evidence administration with guided repairs
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.
2026-09-10 10:31:34 +02:00

196 lines
12 KiB
TypeScript

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]);
});