mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
Invalidate stale artifact diagnostics on detail refresh
This commit is contained in:
parent
d154044cfc
commit
c3d1c7febf
@ -17,7 +17,15 @@ const checkFields = ref<Record<string, string>>({})
|
||||
const checkLoading = ref<Record<string, boolean>>({})
|
||||
const checkErrors = ref<Record<string, string>>({})
|
||||
const checkResults = ref<Record<string, ArtifactJsonCheck>>({})
|
||||
let checkGeneration: Record<string, number> = {}
|
||||
function invalidateCheck(id: string) {
|
||||
checkGeneration[id] = (checkGeneration[id] ?? 0) + 1
|
||||
delete checkLoading.value[id]
|
||||
delete checkErrors.value[id]
|
||||
delete checkResults.value[id]
|
||||
}
|
||||
function clearChecks() {
|
||||
checkGeneration = {}
|
||||
checkFields.value = {}
|
||||
checkLoading.value = {}
|
||||
checkErrors.value = {}
|
||||
@ -66,6 +74,7 @@ async function loadDetail(id: string, event: Event) {
|
||||
if (!(event.target as HTMLDetailsElement).open || detailLoading.value[id]) return
|
||||
const request = generation
|
||||
detailLoading.value[id] = true
|
||||
invalidateCheck(id)
|
||||
delete detailErrors.value[id]
|
||||
try {
|
||||
const { data } = await executionEvidenceApi.get(id)
|
||||
@ -86,7 +95,7 @@ async function loadDetail(id: string, event: Event) {
|
||||
}
|
||||
}
|
||||
async function checkJson(id: string) {
|
||||
if (checkLoading.value[id]) return
|
||||
if (loading.value || detailLoading.value[id] || checkLoading.value[id]) return
|
||||
const fields = (checkFields.value[id] ?? '').split(/\r?\n/).filter(field => field.length > 0)
|
||||
delete checkResults.value[id]
|
||||
delete checkErrors.value[id]
|
||||
@ -96,17 +105,19 @@ async function checkJson(id: string) {
|
||||
return
|
||||
}
|
||||
const request = generation
|
||||
const checkRequest = checkGeneration[id] ?? 0
|
||||
const isCurrent = () => request === generation && checkRequest === (checkGeneration[id] ?? 0)
|
||||
checkLoading.value[id] = true
|
||||
try {
|
||||
const { data } = await executionEvidenceApi.checkJson(id, fields)
|
||||
if (request !== generation || !items.value.some(item => item.id === id)) return
|
||||
if (!isCurrent() || !items.value.some(item => item.id === id)) return
|
||||
checkResults.value[id] = data
|
||||
if (data.status === 'UNAVAILABLE') {
|
||||
items.value = items.value.map(item => item.id === id
|
||||
? { ...item, artifactRef: null, artifactDigest: null, summary: null, validity: 'UNAVAILABLE' } : item)
|
||||
}
|
||||
} catch (error) {
|
||||
if (request !== generation) return
|
||||
if (!isCurrent()) return
|
||||
const failure = error as { code?: number; response?: { status?: number } }
|
||||
const code = failure.response?.status ?? failure.code
|
||||
if (code === 401 || code === 403 || code === 404) {
|
||||
@ -117,7 +128,7 @@ async function checkJson(id: string) {
|
||||
checkErrors.value[id] = code === 400 ? 'executionEvidence.jsonCheck.inputError' : 'executionEvidence.loadError'
|
||||
}
|
||||
} finally {
|
||||
if (request === generation) delete checkLoading.value[id]
|
||||
if (isCurrent()) delete checkLoading.value[id]
|
||||
}
|
||||
}
|
||||
function toggle() {
|
||||
@ -178,10 +189,10 @@ onBeforeUnmount(() => { generation++ })
|
||||
<form v-if="item.kind === 'ARTIFACT_SNAPSHOT' && item.artifactRef" class="json-check" @submit.prevent="checkJson(item.id)">
|
||||
<label :for="`json-fields-${item.id}`">{{ t('executionEvidence.jsonCheck.label') }}</label>
|
||||
<textarea :id="`json-fields-${item.id}`" v-model="checkFields[item.id]" data-json-fields rows="3" maxlength="2064"
|
||||
:disabled="!!checkLoading[item.id]" :placeholder="t('executionEvidence.jsonCheck.placeholder')"
|
||||
:disabled="loading || !!detailLoading[item.id] || !!checkLoading[item.id]" :placeholder="t('executionEvidence.jsonCheck.placeholder')"
|
||||
@input="delete checkResults[item.id]" />
|
||||
<p>{{ t('executionEvidence.jsonCheck.scope') }}</p>
|
||||
<button type="submit" data-json-check :disabled="!!checkLoading[item.id]">{{ t(checkLoading[item.id] ? 'common.loading' : 'executionEvidence.jsonCheck.run') }}</button>
|
||||
<button type="submit" data-json-check :disabled="loading || !!detailLoading[item.id] || !!checkLoading[item.id]">{{ t(checkLoading[item.id] ? 'common.loading' : 'executionEvidence.jsonCheck.run') }}</button>
|
||||
</form>
|
||||
<p v-if="checkErrors[item.id]" role="alert">{{ t(checkErrors[item.id]) }}</p>
|
||||
<div v-if="checkResults[item.id]" data-json-result role="status">
|
||||
|
||||
@ -125,6 +125,54 @@ describe('execution evidence', () => {
|
||||
expect(host.querySelector('[data-json-result]')).toBeNull()
|
||||
expect(host.textContent).not.toContain('All listed fields are present')
|
||||
})
|
||||
it.each([false, true])('invalidates JSON results when artifact details are refreshed (pending=%s)', async (pending) => {
|
||||
const artifact = { ...row('artifact'), kind: 'ARTIFACT_SNAPSHOT', artifactRef: 'file', artifactDigest: 'hash' }
|
||||
const result = { data: { status: 'MATCH', missingFields: [], checkedAt: 'old', acceptanceEligible: false } }
|
||||
let resolveCheck!: (value: unknown) => void
|
||||
vi.mocked(executionEvidenceApi.list).mockResolvedValue({ data: { items: [artifact], nextCursor: null } } as never)
|
||||
vi.mocked(executionEvidenceApi.get).mockResolvedValue({ data: { ...artifact, validity: 'STALE' } } as never)
|
||||
vi.mocked(executionEvidenceApi.checkJson).mockImplementationOnce(() => new Promise(r => { resolveCheck = r }) as never)
|
||||
.mockResolvedValueOnce({ data: { ...result.data, status: 'STALE', checkedAt: 'new' } } as never)
|
||||
const { host } = mount(); host.querySelector<HTMLButtonElement>('[data-evidence-toggle]')!.click(); await flush()
|
||||
const field = host.querySelector<HTMLTextAreaElement>('[data-json-fields]')!
|
||||
field.value = 'report'; field.dispatchEvent(new Event('input')); await flush()
|
||||
host.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); await flush()
|
||||
if (!pending) {
|
||||
resolveCheck(result); await flush()
|
||||
expect(host.querySelector('[data-json-result]')?.textContent).toContain('All listed fields are present')
|
||||
}
|
||||
const details = host.querySelector('details')!; details.open = true; details.dispatchEvent(new Event('toggle')); await flush()
|
||||
expect(host.textContent).toContain(en.executionEvidence.validity.STALE)
|
||||
if (pending) { resolveCheck(result); await flush() }
|
||||
expect(host.querySelector('[data-json-result]')).toBeNull()
|
||||
host.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); await flush()
|
||||
expect(executionEvidenceApi.checkJson).toHaveBeenCalledTimes(2)
|
||||
expect(host.querySelector('[data-json-result]')?.textContent).toContain('new')
|
||||
expect(host.querySelector('[data-json-result]')?.textContent).not.toContain('All listed fields are present')
|
||||
})
|
||||
it('ignores an obsolete check rejection while a replacement check is pending', async () => {
|
||||
const artifact = { ...row('artifact'), kind: 'ARTIFACT_SNAPSHOT', artifactRef: 'file' }
|
||||
let rejectOld!: (reason: unknown) => void
|
||||
let resolveNew!: (value: unknown) => void
|
||||
vi.mocked(executionEvidenceApi.list).mockResolvedValue({ data: { items: [artifact], nextCursor: null } } as never)
|
||||
vi.mocked(executionEvidenceApi.get).mockResolvedValue({ data: artifact } as never)
|
||||
vi.mocked(executionEvidenceApi.checkJson).mockImplementationOnce(() => new Promise((_, reject) => { rejectOld = reject }) as never)
|
||||
.mockImplementationOnce(() => new Promise(resolve => { resolveNew = resolve }) as never)
|
||||
const { host } = mount(); host.querySelector<HTMLButtonElement>('[data-evidence-toggle]')!.click(); await flush()
|
||||
const field = host.querySelector<HTMLTextAreaElement>('[data-json-fields]')!
|
||||
field.value = 'report'; field.dispatchEvent(new Event('input')); await flush()
|
||||
host.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); await flush()
|
||||
const details = host.querySelector('details')!; details.open = true; details.dispatchEvent(new Event('toggle')); await flush()
|
||||
host.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); await flush()
|
||||
expect(executionEvidenceApi.checkJson).toHaveBeenCalledTimes(2)
|
||||
rejectOld({ code: 403 }); await flush()
|
||||
expect(host.querySelector('[data-evidence-item]')).not.toBeNull()
|
||||
expect(host.querySelector<HTMLButtonElement>('[data-json-check]')!.disabled).toBe(true)
|
||||
expect(host.querySelector('[role="alert"]')).toBeNull()
|
||||
resolveNew({ data: { status: 'MATCH', missingFields: [], checkedAt: 'new', acceptanceEligible: false } }); await flush()
|
||||
expect(host.querySelector('[data-json-result]')?.textContent).toContain('new')
|
||||
expect(host.querySelector<HTMLButtonElement>('[data-json-check]')!.disabled).toBe(false)
|
||||
})
|
||||
it('rejects empty JSON requirements locally without reading the file', async () => {
|
||||
vi.mocked(executionEvidenceApi.list).mockResolvedValue({ data: { items: [{ ...row('artifact'), kind: 'ARTIFACT_SNAPSHOT', artifactRef: 'file' }], nextCursor: null } } as never)
|
||||
const { host } = mount(); host.querySelector<HTMLButtonElement>('[data-evidence-toggle]')!.click(); await flush()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user