@@ -8,8 +8,8 @@ import {
|
||||
seedChatHistory,
|
||||
} from "./fixtures/chat-seed";
|
||||
|
||||
const API = process.env.E2E_API_URL ?? "http://localhost:8080";
|
||||
const PLATFORM_URL = process.env.E2E_PLATFORM_URL ?? "http://localhost:8080";
|
||||
const API = process.env.E2E_API_URL ?? PLATFORM_URL;
|
||||
const ADMIN_TOKEN = process.env.E2E_ADMIN_TOKEN ?? process.env.ADMIN_TOKEN;
|
||||
|
||||
/** Enter the Org-map view so the Canvas (React Flow graph) mounts. */
|
||||
@@ -255,9 +255,11 @@ test.describe("Data Flow — Initial Prompt in Chat", () => {
|
||||
const stopHeartbeat = startHeartbeat(ws.id, ws.authToken);
|
||||
|
||||
// Pre-seed chat history so the My Chat panel shows deterministic content.
|
||||
// Include double quotes to regression-test shell-safe JSON quoting in
|
||||
// seedChatHistory (CR2 #11517).
|
||||
await seedChatHistory(workspaceId, [
|
||||
{ role: "user", content: "Hello from seed" },
|
||||
{ role: "agent", content: "Hello back from seed" },
|
||||
{ role: "user", content: 'Hello from seed with "quotes"' },
|
||||
{ role: "agent", content: 'Hello back from seed with "quotes"' },
|
||||
]);
|
||||
|
||||
cleanup = async () => {
|
||||
@@ -277,8 +279,8 @@ test.describe("Data Flow — Initial Prompt in Chat", () => {
|
||||
|
||||
test("seeded chat history appears in My Chat", async ({ page }) => {
|
||||
const panel = page.locator("#panel-chat");
|
||||
await expect(panel.getByText("Hello from seed")).toBeVisible({ timeout: 5_000 });
|
||||
await expect(panel.getByText("Hello back from seed")).toBeVisible({ timeout: 5_000 });
|
||||
await expect(panel.getByText('Hello from seed with "quotes"')).toBeVisible({ timeout: 5_000 });
|
||||
await expect(panel.getByText('Hello back from seed with "quotes"')).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
test("My Chat empty state is not shown when history exists", async ({ page }) => {
|
||||
|
||||
@@ -9,10 +9,45 @@
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const PLATFORM_URL = process.env.E2E_PLATFORM_URL ?? "http://localhost:8080";
|
||||
|
||||
interface PgCredentials {
|
||||
user: string;
|
||||
pass: string;
|
||||
host: string;
|
||||
port: string;
|
||||
db: string;
|
||||
}
|
||||
|
||||
function parseDbUrl(): PgCredentials | null {
|
||||
const dbUrl = process.env.E2E_DATABASE_URL;
|
||||
if (!dbUrl) return null;
|
||||
const pgRegex = /postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/;
|
||||
const m = dbUrl.match(pgRegex);
|
||||
if (!m) return null;
|
||||
const [, user, pass, host, port, db] = m;
|
||||
return { user, pass, host, port, db };
|
||||
}
|
||||
|
||||
function runPsql(sql: string, timeoutMs = 30_000): void {
|
||||
const creds = parseDbUrl();
|
||||
if (!creds) {
|
||||
throw new Error("E2E_DATABASE_URL must be set for DB seeding");
|
||||
}
|
||||
const { user, pass, host, port, db } = creds;
|
||||
execFileSync(
|
||||
"psql",
|
||||
["-h", host, "-p", port, "-U", user, "-d", db, "-c", sql],
|
||||
{
|
||||
env: { ...process.env, PGPASSWORD: pass },
|
||||
stdio: "pipe",
|
||||
timeout: timeoutMs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export interface SeededWorkspace {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -62,16 +97,9 @@ export async function seedWorkspace(echoURL: string): Promise<SeededWorkspace> {
|
||||
// 2. Direct DB update: mark online + point url at echo runtime.
|
||||
// The platform blocks loopback URLs at the API layer (SSRF guard),
|
||||
// so we bypass via psql for local E2E.
|
||||
const dbUrl = process.env.E2E_DATABASE_URL;
|
||||
if (!dbUrl) {
|
||||
if (!process.env.E2E_DATABASE_URL) {
|
||||
throw new Error("E2E_DATABASE_URL must be set for DB seeding");
|
||||
}
|
||||
const pgRegex = /postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/;
|
||||
const m = dbUrl.match(pgRegex);
|
||||
if (!m) {
|
||||
throw new Error(`Cannot parse E2E_DATABASE_URL: ${dbUrl}`);
|
||||
}
|
||||
const [, user, pass, host, port, db] = m;
|
||||
|
||||
// Pre-seed a platform_inbound_secret so chat file uploads don't trigger
|
||||
// the lazy-heal 503 "retry in 30 s" path on first use.
|
||||
@@ -81,17 +109,9 @@ export async function seedWorkspace(echoURL: string): Promise<SeededWorkspace> {
|
||||
],
|
||||
).join("");
|
||||
|
||||
const psql = [
|
||||
`PGPASSWORD=${pass} psql`,
|
||||
`-h ${host} -p ${port} -U ${user} -d ${db}`,
|
||||
`-c "UPDATE workspaces SET status = 'online', url = '${echoURL}', platform_inbound_secret = '${inboundSecret}' WHERE id = '${ws.id}'"`,
|
||||
].join(" ");
|
||||
|
||||
try {
|
||||
execSync(psql, { stdio: "pipe", timeout: 30_000 });
|
||||
} catch (err) {
|
||||
throw new Error(`DB update failed: ${err}`);
|
||||
}
|
||||
runPsql(
|
||||
`UPDATE workspaces SET status = 'online', url = '${echoURL}', platform_inbound_secret = '${inboundSecret}', delivery_mode = 'push' WHERE id = '${ws.id}'`,
|
||||
);
|
||||
|
||||
cacheWorkspaceURL(ws.id, echoURL);
|
||||
|
||||
@@ -152,30 +172,49 @@ export function startHeartbeat(
|
||||
|
||||
/**
|
||||
* Seed chat-history rows for a workspace.
|
||||
*
|
||||
* Chat history is read from activity_logs via messagestore.MessageStore
|
||||
* (workspace-server/internal/messagestore/postgres_store.go). We insert
|
||||
* a2a_receive rows with source_id NULL (canvas-origin) so the
|
||||
* /chat-history hydrator picks them up. Each message becomes its own row
|
||||
* so arbitrary user/agent sequences can be seeded.
|
||||
*/
|
||||
export async function seedChatHistory(
|
||||
workspaceId: string,
|
||||
messages: Array<{ role: "user" | "agent"; content: string }>,
|
||||
): Promise<void> {
|
||||
const dbUrl = process.env.E2E_DATABASE_URL;
|
||||
if (!dbUrl) return;
|
||||
if (!process.env.E2E_DATABASE_URL) return;
|
||||
|
||||
const pgRegex = /postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/;
|
||||
const m = dbUrl.match(pgRegex);
|
||||
if (!m) return;
|
||||
const [, user, pass, host, port, db] = m;
|
||||
const escape = (s: string) => s.replace(/'/g, "''").replace(/\\/g, "\\\\");
|
||||
|
||||
const values = messages
|
||||
.map(
|
||||
(msg, i) =>
|
||||
`('${randomUUID()}', '${workspaceId}', '${msg.role}', '${msg.content.replace(/'/g, "''")}', NOW() - INTERVAL '${messages.length - i} seconds')`,
|
||||
)
|
||||
const rows = messages
|
||||
.map((msg, i) => {
|
||||
const offsetSec = messages.length - i;
|
||||
const requestBody =
|
||||
msg.role === "user"
|
||||
? JSON.stringify({
|
||||
params: {
|
||||
message: {
|
||||
parts: [{ kind: "text", text: msg.content }],
|
||||
},
|
||||
},
|
||||
})
|
||||
: "{}";
|
||||
const responseBody =
|
||||
msg.role === "agent"
|
||||
? JSON.stringify({
|
||||
result: {
|
||||
parts: [{ kind: "text", text: msg.content }],
|
||||
},
|
||||
})
|
||||
: "{}";
|
||||
return `('${randomUUID()}', '${workspaceId}', 'a2a_receive', NULL, NULL, 'message/send', NULL, '${escape(requestBody)}'::jsonb, '${escape(responseBody)}'::jsonb, 0, 'ok', NOW() - INTERVAL '${offsetSec} seconds')`;
|
||||
})
|
||||
.join(",");
|
||||
|
||||
const sql = `INSERT INTO chat_messages (id, workspace_id, role, content, created_at) VALUES ${values};`;
|
||||
const sql = `INSERT INTO activity_logs (id, workspace_id, activity_type, source_id, target_id, method, summary, request_body, response_body, duration_ms, status, created_at) VALUES ${rows};`;
|
||||
|
||||
const psql = `PGPASSWORD=${pass} psql -h ${host} -p ${port} -U ${user} -d ${db} -c "${sql}"`;
|
||||
execSync(psql, { stdio: "pipe", timeout: 10_000 });
|
||||
runPsql(sql, 10_000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,18 +224,10 @@ export async function seedChatHistory(
|
||||
* that can race or 500 on external workspaces.
|
||||
*/
|
||||
export async function cleanupWorkspace(workspaceId: string): Promise<void> {
|
||||
const dbUrl = process.env.E2E_DATABASE_URL;
|
||||
if (!dbUrl) return;
|
||||
|
||||
const pgRegex = /postgres:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/;
|
||||
const m = dbUrl.match(pgRegex);
|
||||
if (!m) return;
|
||||
const [, user, pass, host, port, db] = m;
|
||||
|
||||
const psql = `PGPASSWORD=${pass} psql -h ${host} -p ${port} -U ${user} -d ${db} -c "DELETE FROM workspaces WHERE id = '${workspaceId}'"`;
|
||||
if (!process.env.E2E_DATABASE_URL) return;
|
||||
|
||||
try {
|
||||
execSync(psql, { stdio: "pipe", timeout: 30_000 });
|
||||
runPsql(`DELETE FROM workspaces WHERE id = '${workspaceId}'`);
|
||||
} catch {
|
||||
// Best-effort cleanup; don't fail the test suite if the row is already gone.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user