diff --git a/mateclaw-ui/src/components/execution/ExecutionEvidenceList.vue b/mateclaw-ui/src/components/execution/ExecutionEvidenceList.vue index a8faac2a..af30911a 100644 --- a/mateclaw-ui/src/components/execution/ExecutionEvidenceList.vue +++ b/mateclaw-ui/src/components/execution/ExecutionEvidenceList.vue @@ -48,7 +48,16 @@ async function load(more = false) { if (request !== generation) return const failure = error as { code?: number; response?: { status?: number } } const code = failure.response?.status ?? failure.code - errorKey.value = code === 401 || code === 403 ? 'executionEvidence.accessError' : 'executionEvidence.loadError' + const denied = code === 401 || code === 403 || code === 404 + if (denied) { + items.value = [] + nextCursor.value = null + loaded.value = false + detailLoading.value = {} + detailErrors.value = {} + clearChecks() + } + errorKey.value = denied ? 'executionEvidence.accessError' : 'executionEvidence.loadError' } finally { if (request === generation) loading.value = false } diff --git a/mateclaw-ui/src/components/execution/__tests__/ExecutionEvidenceList.test.ts b/mateclaw-ui/src/components/execution/__tests__/ExecutionEvidenceList.test.ts index 37bf7b5b..dc3222ef 100644 --- a/mateclaw-ui/src/components/execution/__tests__/ExecutionEvidenceList.test.ts +++ b/mateclaw-ui/src/components/execution/__tests__/ExecutionEvidenceList.test.ts @@ -34,6 +34,29 @@ describe('execution evidence', () => { expect(host.querySelector('[role="alert"]')?.textContent).toContain('permission') expect(host.textContent).not.toContain('No execution evidence') }) + it.each([[401, false], [403, true], [404, false]])('clears prior evidence after a denied list request (%s, more=%s)', async (code, more) => { + vi.mocked(executionEvidenceApi.list).mockResolvedValueOnce({ data: { items: [{ ...row('old'), summary: 'previous sensitive evidence' }], nextCursor: 'next' } } as never) + .mockRejectedValueOnce({ code }) + .mockResolvedValueOnce({ data: { items: [row('restored')], nextCursor: null } } as never) + const { host } = mount(); host.querySelector('[data-evidence-toggle]')!.click(); await flush() + const button = more ? host.querySelector('[data-evidence-more]')! + : host.querySelector('.execution-evidence__body > button')! + button.click(); await flush() + expect(host.querySelectorAll('[data-evidence-item]')).toHaveLength(0) + expect(host.textContent).not.toContain('previous sensitive evidence') + expect(host.querySelector('[data-evidence-more]')).toBeNull() + expect(host.querySelector('[role="alert"]')?.textContent).toContain('permission') + host.querySelector('.execution-evidence__body > button')!.click(); await flush() + expect(host.querySelector('[data-evidence-item]')?.getAttribute('data-evidence-item')).toBe('restored') + }) + it('keeps the last loaded page for retry after a transient list error', async () => { + vi.mocked(executionEvidenceApi.list).mockResolvedValueOnce({ data: { items: [row('old')], nextCursor: 'next' } } as never) + .mockRejectedValueOnce({ response: { status: 500 } }) + const { host } = mount(); host.querySelector('[data-evidence-toggle]')!.click(); await flush() + host.querySelector('.execution-evidence__body > button')!.click(); await flush() + expect(host.querySelector('[data-evidence-item]')?.getAttribute('data-evidence-item')).toBe('old') + expect(host.querySelector('[data-evidence-more]')).not.toBeNull() + }) it('ignores a stale response after the selected conversation changes', async () => { let resolve!: (value: unknown) => void vi.mocked(executionEvidenceApi.list).mockImplementationOnce(() => new Promise(r => { resolve = r }) as never).mockResolvedValueOnce({ data: { items: [row('new')], nextCursor: null } } as never)