Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dcf012f
FEAT: Add expandable sidebar navigation
richlundeen Oct 29, 2026
4a72b94
FIX: Preserve sidebar tooltips and share responsive queries
richlundeen Oct 29, 2026
ca81736
FEAT: Consolidate attack and scanner history
richlundeen Oct 29, 2026
22faf6b
FIX: Keep editor drafts until new-attack navigation commits
richlundeen Oct 29, 2026
c8c3a56
FIX: Show unknown routes without losing view state
richlundeen Oct 29, 2026
9bd986a
FIX: Return to retained chat work after inspection
richlundeen Oct 29, 2026
2b4b45d
FEAT: Normalize registry identities
richlundeen Oct 29, 2026
9fc4c51
FIX: Align registry browser checks and identity docs
richlundeen Oct 29, 2026
7f925d7
Merge origin/main and stabilize chat scoring concurrency regression
richlundeen Oct 29, 2026
7279872
Merge origin/main into PR 3650
richlundeen Oct 31, 2026
050678a
Merge PyRIT 3642 dependency into PR 3650
richlundeen Oct 31, 2026
6aa0feb
Merge latest main before PR repair
richlundeen Oct 31, 2026
aa611d7
fix(frontend): repair consolidated history feedback
richlundeen Nov 1, 2026
3b01664
Merge main into PR 3642 repair baseline
richlundeen Nov 1, 2026
1ac07c1
Restore focused chat scoring test waits
richlundeen Nov 1, 2026
a9f4e93
Merge exact origin/main into PR 3650 preserving consolidated navigation
richlundeen Nov 1, 2026
9f85042
Merge prepared PyRIT PR 3642 dependency into PR 3650
richlundeen Nov 1, 2026
b1b7db5
fix: restore consolidated history validation contracts
richlundeen Nov 1, 2026
3221d05
Fix seeded flow setup for feature-owned New attack control
richlundeen Nov 1, 2026
9f5caca
Merge origin/main into PR #3651
richlundeen Nov 1, 2026
1e4014f
Merge prepared PR #3650 dependency into PR #3651
richlundeen Nov 1, 2026
176ffd4
Fix registry filters and target identity inspection
richlundeen Nov 1, 2026
965afdc
Merge branch 'main' into richlundeen-converter-registry
richlundeen Nov 2, 2026
6a1f5e8
Merge selected main into PR #3651
richlundeen Nov 3, 2026
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
6 changes: 6 additions & 0 deletions doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ named techniques that configure existing attacks. Browse configured scorers
and select a default chat scorer; scorer setup uses initializers, not an editor
on this page. Target defaults apply to new chats and scans.

Expand a target, scorer, technique, or converter name to inspect its identity
JSON. Target identity inspection is independent of expanding composite target
members; defaults, visibility, and the target's configuration columns remain
available. Converter inputs and outputs stay visible in the table. Use the small
red X to remove a converter from the live registry; confirmation is required.

**Scenario presets** save a scenario configuration for reuse with a target
chosen at launch. **Suites** maintain ordered collections of preset definitions.
They are stored separately from live component instances.
Expand Down
52 changes: 24 additions & 28 deletions frontend/e2e/registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,18 @@ test("creates and selects a runtime technique with the real backend @seeded", as
await expect(page.getByText("Named configurations of existing attack techniques", { exact: true })).toBeVisible();
await expect(page.getByText(/Runtime only|No attack runs when you add a technique/)).toHaveCount(0);
await expect(page.getByRole("table", { name: "Registered techniques" })).toBeVisible();
const builtinCalls = {
crescendo_simulated: "AttackTechniqueFactory.with_simulated_conversation(\n" +
" name='crescendo_simulated',\n" +
" description='Escalates gradually over a simulated conversation toward the objective.',\n" +
" technique_tags=['single_turn'],\n)",
tap: "AttackTechniqueFactory(\n" +
" name='tap',\n" +
" attack_class=TreeOfAttacksWithPruningAttack,\n" +
" description='Explores a tree of adversarial prompts, pruning weak branches to refine the attack.',\n" +
" technique_tags=['multi_turn'],\n)",
};
for (const [builtinName, statement] of Object.entries(builtinCalls)) {
await page.getByRole("button", { name: `Details for ${builtinName}` }).click();
await expect(page.getByLabel("Technique creation call", { exact: true })).toHaveText(statement);
await expect(page.getByRole("heading", { name: "Configuration" })).toHaveCount(0);
await page.getByRole("button", { name: "Close", exact: true }).click();
for (const builtinName of ["crescendo_simulated", "tap"]) {
const response = await request.get(`/api/techniques/${builtinName}`, { headers });
expect(response.ok(), await response.text()).toBe(true);
const builtin = await response.json();
const toggle = page.getByRole("button", { name: `Identity for ${builtinName}`, exact: true });
await toggle.click();
const identity = page.getByRole("region", { name: `Identity for ${builtinName}`, exact: true });
await expect(identity).toBeVisible();
expect(JSON.parse(await identity.innerText())).toEqual(builtin.identifier);
await expect(page.getByRole("dialog")).toHaveCount(0);
await toggle.press("Enter");
await expect(identity).toHaveCount(0);
}
await page.getByRole("button", { name: "New technique" }).click();
await page.getByRole("textbox", { name: "Registry name" }).fill(name);
Expand Down Expand Up @@ -92,17 +88,17 @@ test("creates and selects a runtime technique with the real backend @seeded", as
expect(body).not.toHaveProperty("seed_technique");
await expect(page.getByRole("dialog")).not.toBeVisible();
await page.getByRole("textbox", { name: "Search techniques" }).fill(name);
await expect(page.getByRole("cell", { name, exact: true })).toBeVisible();
await page.getByRole("button", { name: `Details for ${name}` }).click();
const creationCall = page.getByLabel("Technique creation call", { exact: true });
await expect(creationCall).toHaveText(createdTechnique.creation_statement);
await expect(creationCall).toContainText("AttackTechniqueFactory(");
await expect(creationCall).toContainText("'max_attempts_on_failure': 0");
expect(createdTechnique).not.toHaveProperty("evaluation_identifier");
expect(createdTechnique).not.toHaveProperty("configuration");
await expect(page.getByRole("heading", { name: "Configuration" })).toHaveCount(0);
await page.getByRole("button", { name: "Close", exact: true }).click();
await expect(page.getByRole("button", { name: `Details for ${name}` })).toBeFocused();
const identityToggle = page.getByRole("button", { name: `Identity for ${name}`, exact: true });
await expect(identityToggle).toBeVisible();
await identityToggle.click();
const identity = page.getByRole("region", { name: `Identity for ${name}`, exact: true });
await expect(identity).toBeVisible();
expect(JSON.parse(await identity.innerText())).toEqual(createdTechnique.identifier);
expect(createdTechnique.identifier.kwargs).toMatchObject({ max_attempts_on_failure: 0 });
await expect(page.getByRole("dialog")).toHaveCount(0);
await identityToggle.press("Space");
await expect(identity).toHaveCount(0);
await expect(identityToggle).toBeFocused();
expect(detailRequests).toEqual([]);

let finishCreate: (() => void) | undefined;
Expand Down Expand Up @@ -132,7 +128,7 @@ test("creates and selects a runtime technique with the real backend @seeded", as
const staleResponse = page.waitForResponse((response) => response.url().endsWith("/api/techniques") && response.request().method() === "POST");
finishCreate?.();
await staleResponse;
await expect(page.getByRole("cell", { name: "old_runtime_response", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Identity for old_runtime_response", exact: true })).toHaveCount(0);

const current = await request.get("/api/scenarios/catalog/airt.rapid_response", { headers });
expect(current.ok(), await current.text()).toBe(true);
Expand Down
112 changes: 112 additions & 0 deletions frontend/src/components/Config/TargetTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,118 @@ describe('TargetTable', () => {
expect(screen.getAllByText('TextTarget').length).toBeGreaterThanOrEqual(1)
})

it('should inspect each target identity independently and preserve defaults and visibility', async () => {
const user = userEvent.setup()
const first = makeTarget({
target_registry_name: 'target_one',
target_type: 'OpenAIChatTarget',
identifier: {
constructor_params: { temperature: 0.4 },
__children__: { target: { class_name: 'InnerTarget', hash: 'inner-hash' } },
},
})
const second = makeTarget({
target_registry_name: 'target_two',
target_type: 'TextTarget',
identifier: { constructor_params: { prefix: 'two' } },
})
const updatedFirst = makeTarget({
...first,
identifier: { ...first.identifier, constructor_params: { temperature: 0.8 } },
})
const { rerender } = render(
<TestWrapper>
<TargetTable
{...defaultProps}
targets={[first, second]}
defaultObjectiveTarget={first}
/>
</TestWrapper>
)
const firstToggle = screen.getByRole('button', { name: 'Identity for target_one' })
const secondToggle = screen.getByRole('button', { name: 'Identity for target_two' })
expect(firstToggle).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByRole('region', { name: 'Identity for target_one' })).not.toBeInTheDocument()

await user.click(firstToggle)
await user.click(secondToggle)
const firstDetails = screen.getByRole('region', { name: 'Identity for target_one' })
const secondDetails = screen.getByRole('region', { name: 'Identity for target_two' })
expect(JSON.parse(firstDetails.textContent ?? '')).toEqual(first.identifier)
expect(JSON.parse(secondDetails.textContent ?? '')).toEqual(second.identifier)
expect(firstToggle).toHaveAttribute('aria-controls', firstDetails.id)
expect(secondToggle).toHaveAttribute('aria-controls', secondDetails.id)
expect(firstDetails.id).not.toBe(secondDetails.id)
expect(within(screen.getByRole('table')).getByLabelText('Default objective target')).toHaveTextContent('Objective')
expect(defaultProps.onSetDefaultObjectiveTarget).not.toHaveBeenCalled()
expect(defaultProps.onSetDefaultAdversarialTarget).not.toHaveBeenCalled()

rerender(
<TestWrapper>
<TargetTable
{...defaultProps}
targets={[updatedFirst, second]}
defaultObjectiveTarget={updatedFirst}
/>
</TestWrapper>
)
expect(screen.getByRole('region', { name: 'Identity for target_one' })).toHaveTextContent('"temperature": 0.8')
expect(screen.getByRole('region', { name: 'Identity for target_two' })).toBeVisible()

await user.click(screen.getByRole('button', { name: 'Hide target_one' }))
expect(screen.queryByRole('button', { name: 'Identity for target_one' })).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Show hidden targets (1)' })).toBeInTheDocument()
expect(defaultProps.onSetDefaultObjectiveTarget).not.toHaveBeenCalled()
await user.click(screen.getByRole('checkbox', { name: 'Show hidden targets (1)' }))
expect(screen.getByRole('region', { name: 'Identity for target_one' })).toBeVisible()
})

it('should expand target identity independently from composite members and span all columns', async () => {
const user = userEvent.setup()
const roundRobin = makeTarget({
target_registry_name: 'rr_target',
target_type: 'RoundRobinTarget',
target_specific_params: { weights: [0.6, 0.4] },
identifier: {
constructor_params: { weights: [0.6, 0.4] },
__children__: {
targets: [
{ class_name: 'OpenAIChatTarget', hash: 'first-child' },
{ class_name: 'OpenAIChatTarget', hash: 'second-child' },
],
},
},
inner_targets: [
{ target_registry_name: 'inner_a', target_type: 'OpenAIChatTarget', endpoint: 'https://a.example.test' },
{ target_registry_name: 'inner_b', target_type: 'OpenAIChatTarget', endpoint: 'https://b.example.test' },
],
})
render(
<TestWrapper>
<TargetTable {...defaultProps} targets={[roundRobin]} />
</TestWrapper>
)

const identityToggle = screen.getByRole('button', { name: 'Identity for rr_target' })
await user.click(identityToggle)
const details = screen.getByRole('region', { name: 'Identity for rr_target' })
expect(JSON.parse(details.textContent ?? '')).toEqual(roundRobin.identifier)
expect(details.closest('td')).toHaveAttribute('colspan', String(screen.getAllByRole('columnheader').length))

await user.click(screen.getByRole('button', { name: 'Expand inner targets' }))
expect(screen.getByText('https://a.example.test')).toBeInTheDocument()
expect(screen.getByText('https://b.example.test')).toBeInTheDocument()
expect(screen.getByText('weight: 0.6')).toBeInTheDocument()
await user.click(identityToggle)
expect(screen.queryByRole('region', { name: 'Identity for rr_target' })).not.toBeInTheDocument()
expect(screen.getByText('https://a.example.test')).toBeInTheDocument()

await user.click(identityToggle)
await user.click(screen.getByRole('button', { name: 'Collapse inner targets' }))
expect(screen.getByRole('region', { name: 'Identity for rr_target' })).toBeVisible()
expect(screen.queryByText('https://a.example.test')).not.toBeInTheDocument()
})

it('should display Registry Name, Type, Model, Endpoint, Inputs, Outputs, capability columns and Parameters columns', () => {
render(
<TestWrapper>
Expand Down
Loading
Loading