import { html, fixture, expect, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import './tool-list-item';
import type { ToolListItem } from './tool-list-item';
/**
* Regression tests for per-tool justification settings in tool-list-item.
*
* Covers:
* - Opening the justification dialog populates current mode
* - Saving with an existing config_id calls updateToolConfiguration
* - Saving without config_id creates a new configuration first
* - Disabled mode sends null as justification_mode
* - tool-updated event fires after save
*/
describe('ToolListItem – justification settings', () => {
let fetchStub: sinon.SinonStub;
const baseTool = {
name: 'bash',
description: 'Execute shell commands',
source: 'builtin' as const,
source_id: null,
source_name: 'Built-in',
schema: {},
is_enabled: true,
is_supported: true,
approval_workflow_id: null,
has_approval_condition: false,
config_id: null as string | null,
justification_mode: null as string | null,
};
beforeEach(() => {
localStorage.setItem('accessToken', 'test-access-token');
localStorage.setItem('refreshToken', 'test-refresh-token');
fetchStub = sinon.stub(window, 'fetch');
});
afterEach(() => {
fetchStub.restore();
localStorage.clear();
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function stubApi(opts?: { configId?: string }) {
fetchStub.callsFake(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input.toString();
const method = (init?.method || 'GET').toUpperCase();
// Create tool configuration
if (url.endsWith('/api/v1/tool-configurations') && method === 'POST') {
return new Response(
JSON.stringify({ id: opts?.configId || 'new-cfg-1' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
// Update tool configuration
if (url.includes('/api/v1/tool-configurations/') && method === 'PUT') {
return new Response(
JSON.stringify({ id: opts?.configId || 'cfg-1' }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
return new Response(JSON.stringify({}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
);
}
async function createItem(toolOverrides: Partial<typeof baseTool> = {}) {
const tool = { ...baseTool, ...toolOverrides };
const el = (await fixture(
html`<tool-list-item
.tool=${tool}
.accessRules=${[]}
.policies=${[]}
.features=${{}}
></tool-list-item>`
)) as ToolListItem;
await el.updateComplete;
return el;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
it('opens justification dialog with current mode populated', async () => {
stubApi();
const el = await createItem({ justification_mode: 'required' });
(el as any)._openJustificationDialog();
await el.updateComplete;
expect((el as any)._showJustificationDialog).to.be.true;
expect((el as any)._justificationMode).to.equal('required');
});
it('defaults justification mode to disabled when tool has no mode', async () => {
stubApi();
const el = await createItem({ justification_mode: null });
(el as any)._openJustificationDialog();
await el.updateComplete;
expect((el as any)._justificationMode).to.equal('disabled');
});
it('creates new config when saving justification on tool without config_id', async () => {
stubApi({ configId: 'new-cfg-1' });
const el = await createItem({ config_id: null });
(el as any)._justificationMode = 'required';
await (el as any)._saveJustificationMode();
const createCall = fetchStub.getCalls().find((c) => {
const url = String(c.args[0]);
const method = String(
(c.args[1] as RequestInit | undefined)?.method || 'GET'
).toUpperCase();
return url.endsWith('/api/v1/tool-configurations') && method === 'POST';
});
expect(createCall).to.exist;
const body = JSON.parse(
(createCall!.args[1] as RequestInit).body as string
);
expect(body.justification_mode).to.equal('required');
expect(body.tool_name).to.equal('bash');
});
it('updates existing config when config_id is present', async () => {
stubApi({ configId: 'cfg-existing' });
const el = await createItem({ config_id: 'cfg-existing' });
(el as any)._justificationMode = 'optional';
await (el as any)._saveJustificationMode();
const updateCall = fetchStub.getCalls().find((c) => {
const url = String(c.args[0]);
const method = String(
(c.args[1] as RequestInit | undefined)?.method || 'GET'
).toUpperCase();
return (
url.includes('/api/v1/tool-configurations/cfg-existing') &&
method === 'PUT'
);
});
expect(updateCall).to.exist;
const body = JSON.parse(
(updateCall!.args[1] as RequestInit).body as string
);
expect(body.justification_mode).to.equal('optional');
});
it('sends null justification_mode when mode is disabled', async () => {
stubApi({ configId: 'cfg-1' });
const el = await createItem({ config_id: 'cfg-1' });
(el as any)._justificationMode = 'disabled';
await (el as any)._saveJustificationMode();
const updateCall = fetchStub.getCalls().find((c) => {
const url = String(c.args[0]);
const method = String(
(c.args[1] as RequestInit | undefined)?.method || 'GET'
).toUpperCase();
return url.includes('/api/v1/tool-configurations/') && method === 'PUT';
});
expect(updateCall).to.exist;
const body = JSON.parse(
(updateCall!.args[1] as RequestInit).body as string
);
expect(body.justification_mode).to.be.null;
});
it('dispatches tool-updated event after save', async () => {
stubApi({ configId: 'cfg-1' });
const el = await createItem({ config_id: 'cfg-1' });
let eventFired = false;
el.addEventListener('tool-updated', () => {
eventFired = true;
});
(el as any)._justificationMode = 'required';
await (el as any)._saveJustificationMode();
expect(eventFired).to.be.true;
});
it('closes dialog after successful save', async () => {
stubApi({ configId: 'cfg-1' });
const el = await createItem({ config_id: 'cfg-1' });
(el as any)._showJustificationDialog = true;
(el as any)._justificationMode = 'optional';
await (el as any)._saveJustificationMode();
expect((el as any)._showJustificationDialog).to.be.false;
});
});