Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/e2e/_targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Shared helpers for building target-instance mock payloads in e2e tests.
//
// Mirrors the frontend `makeTarget` fixture (src/test-utils/targetFixtures.ts):
// folds flat identity scalars into the embedded `identifier` so route mocks
// match the current nested `TargetInstance` wire model. Without this, the
// target table crashes reading `target.identifier.class_name` and no rows
// (and no "Set Active" button) render.

export interface FlatTarget {
target_registry_name: string;
target_type?: string;
endpoint?: string | null;
model_name?: string | null;
capabilities?: unknown;
target_specific_params?: unknown;
inner_targets?: FlatTarget[] | null;
}

interface TargetInstanceMock {
target_registry_name: string;
identifier: {
class_name: string;
hash: string;
endpoint: string | null;
model_name: string | null;
};
capabilities: unknown;
target_specific_params: unknown;
inner_targets: TargetInstanceMock[] | null;
}

export function makeTarget(flat: FlatTarget): TargetInstanceMock {
return {
target_registry_name: flat.target_registry_name,
identifier: {
class_name: flat.target_type ?? "TextTarget",
hash: `${flat.target_registry_name}-hash`,
endpoint: flat.endpoint ?? null,
model_name: flat.model_name ?? null,
},
capabilities: flat.capabilities ?? null,
target_specific_params: flat.target_specific_params ?? null,
inner_targets: flat.inner_targets ? flat.inner_targets.map(makeTarget) : null,
};
}
13 changes: 7 additions & 6 deletions frontend/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect } from "@playwright/test";
import { makeTarget } from "./_targets";

test.describe("Accessibility", () => {
test.beforeEach(async ({ page }) => {
Expand All @@ -13,12 +14,12 @@ test.describe("Accessibility", () => {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "a11y-form-target",
target_type: "OpenAIChatTarget",
endpoint: "https://test.com",
model_name: "gpt-4o",
},
}),
],
pagination: { limit: 200, has_more: false, next_cursor: null, prev_cursor: null },
}),
Expand Down Expand Up @@ -84,12 +85,12 @@ test.describe("Accessibility", () => {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "a11y-focus-target",
target_type: "OpenAIChatTarget",
endpoint: "https://test.com",
model_name: "gpt-4o",
},
}),
],
pagination: { limit: 200, has_more: false, next_cursor: null, prev_cursor: null },
}),
Expand Down Expand Up @@ -124,12 +125,12 @@ test.describe("Accessibility", () => {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "a11y-test-target",
target_type: "OpenAIChatTarget",
endpoint: "https://test.com",
model_name: "gpt-4o",
},
}),
],
pagination: { limit: 200, has_more: false, next_cursor: null, prev_cursor: null },
}),
Expand Down
11 changes: 6 additions & 5 deletions frontend/e2e/chat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect, type Page } from "@playwright/test";
import { makeTarget } from "./_targets";

// ---------------------------------------------------------------------------
// Helpers – mock backend API responses so tests don't require an OpenAI key
Expand All @@ -19,12 +20,12 @@ async function mockBackendAPIs(page: Page) {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "mock-openai-chat",
target_type: "OpenAIChatTarget",
endpoint: "https://mock.openai.com",
model_name: "gpt-4o-mock",
},
}),
],
}),
});
Expand Down Expand Up @@ -327,12 +328,12 @@ function buildModalityMock(
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "mock-target",
target_type: "OpenAIChatTarget",
endpoint: "https://mock.endpoint.com",
model_name: "test-model",
},
}),
],
}),
});
Expand Down Expand Up @@ -677,7 +678,7 @@ test.describe("Target type scenarios", () => {
endpoint: "https://api.openai.com",
model_name: "tts-1-hd",
},
];
].map(makeTarget);

test("should list multiple target types on config page", async ({ page }) => {
await page.route(/\/api\/targets/, async (route) => {
Expand Down
7 changes: 4 additions & 3 deletions frontend/e2e/config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { test, expect, type Page } from "@playwright/test";
import { makeTarget, type FlatTarget } from "./_targets";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Return a mock targets list response. */
function mockTargetsList(items: Record<string, unknown>[] = []) {
function mockTargetsList(items: FlatTarget[] = []) {
return {
status: 200,
contentType: "application/json",
body: JSON.stringify({
items,
items: items.map(makeTarget),
pagination: { limit: 200, has_more: false, next_cursor: null, prev_cursor: null },
}),
};
Expand Down Expand Up @@ -138,7 +139,7 @@ test.describe("Target Configuration Page", () => {

test.describe("Create Target Dialog", () => {
test("should create a target through the dialog", async ({ page }) => {
let createdTarget: Record<string, unknown> | null = null;
let createdTarget: FlatTarget | null = null;

await page.route(/\/api\/targets/, async (route) => {
if (route.request().method() === "POST") {
Expand Down
5 changes: 3 additions & 2 deletions frontend/e2e/converters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect, type Page } from "@playwright/test";
import { makeTarget } from "./_targets";

// ---------------------------------------------------------------------------
// Mock data
Expand Down Expand Up @@ -186,7 +187,7 @@ async function mockBackendAPIs(page: Page) {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "mock-openai-chat",
target_type: "OpenAIChatTarget",
endpoint: "https://mock.openai.com",
Expand All @@ -201,7 +202,7 @@ async function mockBackendAPIs(page: Page) {
supported_input_data_types: ["text"],
supported_output_data_types: ["text"],
},
},
}),
],
pagination: { limit: 50, has_more: false },
}),
Expand Down
9 changes: 5 additions & 4 deletions frontend/e2e/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect, type Page, type Route } from "@playwright/test";
import { makeTarget } from "./_targets";

// ---------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -67,12 +68,12 @@ async function mockAllAPIs(
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "mock-target",
target_type: "OpenAIChatTarget",
endpoint: "https://mock.endpoint.com",
model_name: "gpt-4o-mock",
},
}),
],
}),
});
Expand Down Expand Up @@ -438,12 +439,12 @@ test.describe("Error: create attack fails", () => {
contentType: "application/json",
body: JSON.stringify({
items: [
{
makeTarget({
target_registry_name: "mock-target",
target_type: "OpenAIChatTarget",
endpoint: "https://mock.endpoint.com",
model_name: "gpt-4o-mock",
},
}),
],
}),
});
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ jest.mock("./components/Chat/ChatWindow", () => {
});

jest.mock("./components/Config/TargetConfig", () => {
const { makeTarget } = jest.requireActual("@/test-utils/targetFixtures") as typeof import("@/test-utils/targetFixtures");
const MockTargetConfig = ({
activeTarget,
onSetActiveTarget,
Expand All @@ -181,12 +182,10 @@ jest.mock("./components/Config/TargetConfig", () => {
</span>
<button
onClick={() =>
onSetActiveTarget({
target_id: "t1",
onSetActiveTarget(makeTarget({
target_registry_name: "test_target",
target_type: "OpenAIChatTarget",
status: "active",
})
}))
}
data-testid="set-target"
>
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DEFAULT_GLOBAL_LABELS } from './components/Labels/labelDefaults'
import { filtersFromSearchParams, filtersToSearchParams } from './components/History/historyFilters'
import type { ViewName } from './components/Sidebar/Navigation'
import type { TargetInstance, TargetInfo } from './types'
import { targetEndpoint, targetModelName, targetType } from './utils/targetIdentity'
import { attacksApi, versionApi } from './services/api'
import { toApiError } from './services/errors'
import { useTour } from './hooks/useTour'
Expand Down Expand Up @@ -166,9 +167,9 @@ function App() {
setActiveTarget(prev => {
const isSame = prev &&
prev.target_registry_name === target.target_registry_name &&
prev.target_type === target.target_type &&
(prev.endpoint ?? '') === (target.endpoint ?? '') &&
(prev.model_name ?? '') === (target.model_name ?? '')
targetType(prev) === targetType(target) &&
(targetEndpoint(prev) ?? '') === (targetEndpoint(target) ?? '') &&
(targetModelName(prev) ?? '') === (targetModelName(target) ?? '')
if (isSame) return prev
// Switching targets no longer clears the loaded attack. The cross-target
// guard in ChatWindow prevents sending to a mismatched target, and the
Expand Down Expand Up @@ -280,9 +281,9 @@ function App() {
// its next fetch for this id, so the attack opens without a redundant load.
const target: TargetInfo | null = activeTarget
? {
target_type: activeTarget.target_type,
endpoint: activeTarget.endpoint,
model_name: activeTarget.model_name,
target_type: targetType(activeTarget),
endpoint: targetEndpoint(activeTarget),
model_name: targetModelName(activeTarget),
}
: null
skipNextLoadForAttackId.current = arId
Expand Down Expand Up @@ -405,7 +406,7 @@ function App() {
context={{
app_version: appVersion || undefined,
current_view: currentView,
target_type: activeTarget?.target_type,
target_type: activeTarget ? targetType(activeTarget) : undefined,
}}
/>
)}
Expand Down
Loading
Loading