From 0af67f3281c72118debb3485150847b67253d45a Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Mon, 5 Oct 2026 07:40:54 -0300 Subject: [PATCH 1/2] feat(agent-config): plugin tabs and add-plugin dialog Layer 18 of 21 in the stacked split of compliance-framework/ui#318. Co-Authored-By: Claude Opus 5.5 --- .../config/AgentConfigEffectiveView.vue | 151 ++++++++++ src/components/agents/config/PluginTabs.vue | 263 ++++++++++++++++++ .../AgentConfigEffectiveView.spec.ts | 123 ++++++++ .../config/__tests__/PluginLibVersion.spec.ts | 54 ++++ .../agents/config/editor/AddPluginAction.vue | 57 ++++ .../agents/config/editor/AddPluginDialog.vue | 205 ++++++++++++++ 6 files changed, 853 insertions(+) create mode 100644 src/components/agents/config/AgentConfigEffectiveView.vue create mode 100644 src/components/agents/config/PluginTabs.vue create mode 100644 src/components/agents/config/__tests__/AgentConfigEffectiveView.spec.ts create mode 100644 src/components/agents/config/__tests__/PluginLibVersion.spec.ts create mode 100644 src/components/agents/config/editor/AddPluginAction.vue create mode 100644 src/components/agents/config/editor/AddPluginDialog.vue diff --git a/src/components/agents/config/AgentConfigEffectiveView.vue b/src/components/agents/config/AgentConfigEffectiveView.vue new file mode 100644 index 00000000..5d6a9796 --- /dev/null +++ b/src/components/agents/config/AgentConfigEffectiveView.vue @@ -0,0 +1,151 @@ + + + diff --git a/src/components/agents/config/PluginTabs.vue b/src/components/agents/config/PluginTabs.vue new file mode 100644 index 00000000..4e4b13eb --- /dev/null +++ b/src/components/agents/config/PluginTabs.vue @@ -0,0 +1,263 @@ + + + diff --git a/src/components/agents/config/__tests__/AgentConfigEffectiveView.spec.ts b/src/components/agents/config/__tests__/AgentConfigEffectiveView.spec.ts new file mode 100644 index 00000000..16bf2c84 --- /dev/null +++ b/src/components/agents/config/__tests__/AgentConfigEffectiveView.spec.ts @@ -0,0 +1,123 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { mount, enableAutoUnmount } from '@vue/test-utils'; +import type { ConfigDoc, OverlayDoc } from '@/types/agent-config'; +import { baseConfig } from '@/composables/agent-config/__tests__/fixtures'; +import { mergePatch } from '@/utils/agent-config/merge-patch'; +import AgentConfigEffectiveView from '../AgentConfigEffectiveView.vue'; +import { globalWith, piniaWith, READER } from './helpers'; + +// PrimeVue's TabList schedules a 150 ms ink-bar update on mount and never clears it; a wrapper +// left mounted lets it fire after this file's jsdom environment is torn down +// ("HTMLElement is not defined"). Unmounting nulls its refs, so the timer becomes a no-op. +enableAutoUnmount(afterEach); + +function mountView( + overlay: OverlayDoc, + effectiveOverride?: ConfigDoc, + note: number | null = null, +) { + const effective = + effectiveOverride ?? mergePatch(baseConfig, overlay); + return mount(AgentConfigEffectiveView, { + props: { + effectiveDoc: effective, + base: baseConfig, + appliedOverlay: overlay, + appliedRevisionNote: note, + filename: 'a-effective-r7.yaml', + }, + global: globalWith(piniaWith(READER)), + }); +} + +describe('AgentConfigEffectiveView', () => { + it('renders provenance badges for file / overlay / overrides / removed', () => { + const wrapper = mountView({ + verbosity: 2, + plugins: { + 'ubuntu-packages': null, + extra: { source: 'ghcr.io/compliance-framework/extra:v1' }, + 'local-ssh': { schedule: '@hourly' }, + }, + }); + const ssh = wrapper.find('[data-test="plugin-card-local-ssh"]'); + expect(ssh.find('[data-provenance="overrides-file"]').exists()).toBe(true); + const extra = wrapper.find('[data-test="plugin-card-extra"]'); + expect(extra.find('[data-provenance="overlay"]').exists()).toBe(true); + const removed = wrapper.find('[data-test="plugin-card-ubuntu-packages"]'); + expect(removed.text()).toContain('Removed by overlay'); + expect( + removed.find('[data-provenance="removed-by-overlay"]').exists(), + ).toBe(true); + const flags = wrapper.find('[data-test="flags-summary"]'); + expect(flags.text()).toContain('Trace'); + expect(flags.find('[data-provenance="overrides-file"]').exists()).toBe( + true, + ); + }); + + it('shows plain file provenance when the applied overlay is empty, and the pending note', () => { + const wrapper = mountView({}, undefined, 6); + expect( + wrapper + .find('[data-test="plugin-card-local-ssh"] [data-provenance="file"]') + .exists(), + ).toBe(true); + expect(wrapper.find('[data-test="provenance-note"]').text()).toContain( + 'r6', + ); + }); + + it('shows the forbidden keys locked and muted (R71) and never renders client_secret', () => { + const leaky = mergePatch(baseConfig, {}) as ConfigDoc; + leaky.api = { + url: 'https://x', + auth: { client_id: 'cid', client_secret: 'SUPER-SECRET' }, + }; + const wrapper = mountView({}, leaky); + const locked = wrapper.find('[data-test="locked-keys"]'); + expect(locked.text()).toContain('api.auth.client_id'); + expect(locked.text()).toContain('cid'); + expect(locked.findAll('[data-test="lock-icon"]').length).toBe(8); + expect( + locked.find('[data-test="lock-icon"]').attributes('aria-label'), + ).toBe('Set on the agent host; can never be changed remotely'); + expect(locked.findAll('[data-state="forbidden"]').length).toBe(8); + // No pencil on a forbidden field. + expect(locked.find('[data-test^="edit-"]').exists()).toBe(false); + expect(wrapper.html()).not.toContain('SUPER-SECRET'); + }); + + it('never renders client_secret in YAML mode either', async () => { + const leaky = mergePatch(baseConfig, {}) as ConfigDoc; + leaky.api = { + url: 'https://x', + auth: { client_id: 'cid', client_secret: 'SUPER-SECRET' }, + }; + const wrapper = mountView({}, leaky); + wrapper + .findComponent({ name: 'SelectButton' }) + .vm.$emit('update:modelValue', 'yaml'); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="yaml-text"]').text()).toContain( + 'client_id: cid', + ); + expect(wrapper.html()).not.toContain('SUPER-SECRET'); + }); + + it('shows the not-reported text without an effective config', () => { + const wrapper = mount(AgentConfigEffectiveView, { + props: { + effectiveDoc: null, + base: null, + appliedOverlay: null, + appliedRevisionNote: null, + filename: 'x.yaml', + }, + global: globalWith(piniaWith(READER)), + }); + expect(wrapper.find('[data-test="effective-empty"]').text()).toContain( + 'No configuration reported yet.', + ); + }); +}); diff --git a/src/components/agents/config/__tests__/PluginLibVersion.spec.ts b/src/components/agents/config/__tests__/PluginLibVersion.spec.ts new file mode 100644 index 00000000..956617f4 --- /dev/null +++ b/src/components/agents/config/__tests__/PluginLibVersion.spec.ts @@ -0,0 +1,54 @@ +// R76 on the Configuration tab: the plugin's agent library on the Effective view. +import { afterEach, describe, expect, it } from 'vitest'; +import { mount, enableAutoUnmount } from '@vue/test-utils'; +import { + SSH_SOURCE, + baseConfig, +} from '@/composables/agent-config/__tests__/fixtures'; +import type { ConfigDoc, PluginReport } from '@/types/agent-config'; +import AgentConfigEffectiveView from '../AgentConfigEffectiveView.vue'; +import { READER, globalWith, piniaWith } from './helpers'; + +// PrimeVue's TabList schedules a 150 ms ink-bar update on mount and never clears it; a wrapper +// left mounted lets it fire after this file's jsdom environment is torn down +// ("HTMLElement is not defined"). Unmounting nulls its refs, so the timer becomes a no-op. +enableAutoUnmount(afterEach); + +describe('plugin agent library badge (R76)', () => { + function view(plugins: PluginReport[] | null) { + return mount(AgentConfigEffectiveView, { + props: { + effectiveDoc: baseConfig as ConfigDoc, + base: baseConfig, + appliedOverlay: {}, + appliedRevisionNote: null, + filename: 'x.yaml', + pluginReports: plugins, + }, + global: globalWith(piniaWith(READER)), + }); + } + + it('shows the reported library, and nothing when it is unknown', () => { + const w = view([ + { name: 'local-ssh', source: SSH_SOURCE, libVersion: 'v0.1.9' }, + { name: 'ubuntu-packages', libVersion: '' }, + ]); + expect( + w + .find('[data-test="plugin-card-local-ssh"] [data-test="plugin-lib"]') + .text(), + ).toBe('agent v0.1.9'); + expect( + w + .find( + '[data-test="plugin-card-ubuntu-packages"] [data-test="plugin-lib"]', + ) + .exists(), + ).toBe(false); + }); + + it('shows nothing for agents that do not report plugins', () => { + expect(view(null).find('[data-test="plugin-lib"]').exists()).toBe(false); + }); +}); diff --git a/src/components/agents/config/editor/AddPluginAction.vue b/src/components/agents/config/editor/AddPluginAction.vue new file mode 100644 index 00000000..43c4bf75 --- /dev/null +++ b/src/components/agents/config/editor/AddPluginAction.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/agents/config/editor/AddPluginDialog.vue b/src/components/agents/config/editor/AddPluginDialog.vue new file mode 100644 index 00000000..c4d1c6b9 --- /dev/null +++ b/src/components/agents/config/editor/AddPluginDialog.vue @@ -0,0 +1,205 @@ + + + + + From c5da8fa3903c95bedde68f6f2cdfa3d3ab2f4d57 Mon Sep 17 00:00:00 2001 From: "ccf-lisa[bot]" <286799724+ccf-lisa[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:23:59 -0300 Subject: [PATCH 2/2] fix(agent-config): plugin tabs by own name; Add plugin never replaces a defined plugin - the tab's removal hint and the "Removed by overlay" card read plugins as own properties (getOwn), so a plugin named "constructor" is matched like any other - Add plugin refuses every name the saved overlay or the draft defines (non-null), not only the shown cards and loaded files: adding writes the whole plugin and replaced a saved definition this instance does not run yet - Add plugin waits, with the reason, until every reporting instance's file is loaded; the Effective view says files are loading, or names the ones that failed with a Retry Co-Authored-By: Claude Opus 5.5 --- .../config/AgentConfigEffectiveView.vue | 47 +++++- src/components/agents/config/PluginTabs.vue | 17 +- .../AgentConfigEffectiveView.spec.ts | 23 +++ .../config/__tests__/PluginLibVersion.spec.ts | 5 +- .../__tests__/PluginTabsWorkspace.spec.ts | 158 ++++++++++++++++++ .../agents/config/editor/AddPluginAction.vue | 14 +- 6 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 src/components/agents/config/__tests__/PluginTabsWorkspace.spec.ts diff --git a/src/components/agents/config/AgentConfigEffectiveView.vue b/src/components/agents/config/AgentConfigEffectiveView.vue index 5d6a9796..b5b24eda 100644 --- a/src/components/agents/config/AgentConfigEffectiveView.vue +++ b/src/components/agents/config/AgentConfigEffectiveView.vue @@ -8,6 +8,32 @@ {{ NOT_REPORTED_TEXT }}