mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +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 checkLoading = ref<Record<string, boolean>>({})
|
||||||
const checkErrors = ref<Record<string, string>>({})
|
const checkErrors = ref<Record<string, string>>({})
|
||||||
const checkResults = ref<Record<string, ArtifactJsonCheck>>({})
|
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() {
|
function clearChecks() {
|
||||||
|
checkGeneration = {}
|
||||||
checkFields.value = {}
|
checkFields.value = {}
|
||||||
checkLoading.value = {}
|
checkLoading.value = {}
|
||||||
checkErrors.value = {}
|
checkErrors.value = {}
|
||||||
@ -66,6 +74,7 @@ async function loadDetail(id: string, event: Event) {
|
|||||||
if (!(event.target as HTMLDetailsElement).open || detailLoading.value[id]) return
|
if (!(event.target as HTMLDetailsElement).open || detailLoading.value[id]) return
|
||||||
const request = generation
|
const request = generation
|
||||||
detailLoading.value[id] = true
|
detailLoading.value[id] = true
|
||||||
|
invalidateCheck(id)
|
||||||
delete detailErrors.value[id]
|
delete detailErrors.value[id]
|
||||||
try {
|
try {
|
||||||
const { data } = await executionEvidenceApi.get(id)
|
const { data } = await executionEvidenceApi.get(id)
|
||||||
@ -86,7 +95,7 @@ async function loadDetail(id: string, event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function checkJson(id: string) {
|
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)
|
const fields = (checkFields.value[id] ?? '').split(/\r?\n/).filter(field => field.length > 0)
|
||||||
delete checkResults.value[id]
|
delete checkResults.value[id]
|
||||||
delete checkErrors.value[id]
|
delete checkErrors.value[id]
|
||||||
@ -96,17 +105,19 @@ async function checkJson(id: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const request = generation
|
const request = generation
|
||||||
|
const checkRequest = checkGeneration[id] ?? 0
|
||||||
|
const isCurrent = () => request === generation && checkRequest === (checkGeneration[id] ?? 0)
|
||||||
checkLoading.value[id] = true
|
checkLoading.value[id] = true
|
||||||
try {
|
try {
|
||||||
const { data } = await executionEvidenceApi.checkJson(id, fields)
|
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
|
checkResults.value[id] = data
|
||||||
if (data.status === 'UNAVAILABLE') {
|
if (data.status === 'UNAVAILABLE') {
|
||||||
items.value = items.value.map(item => item.id === id
|
items.value = items.value.map(item => item.id === id
|
||||||
? { ...item, artifactRef: null, artifactDigest: null, summary: null, validity: 'UNAVAILABLE' } : item)
|
? { ...item, artifactRef: null, artifactDigest: null, summary: null, validity: 'UNAVAILABLE' } : item)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (request !== generation) return
|
if (!isCurrent()) return
|
||||||
const failure = error as { code?: number; response?: { status?: number } }
|
const failure = error as { code?: number; response?: { status?: number } }
|
||||||
const code = failure.response?.status ?? failure.code
|
const code = failure.response?.status ?? failure.code
|
||||||
if (code === 401 || code === 403 || code === 404) {
|
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'
|
checkErrors.value[id] = code === 400 ? 'executionEvidence.jsonCheck.inputError' : 'executionEvidence.loadError'
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (request === generation) delete checkLoading.value[id]
|
if (isCurrent()) delete checkLoading.value[id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function toggle() {
|
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)">
|
<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>
|
<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"
|
<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]" />
|
@input="delete checkResults[item.id]" />
|
||||||
<p>{{ t('executionEvidence.jsonCheck.scope') }}</p>
|
<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>
|
</form>
|
||||||
<p v-if="checkErrors[item.id]" role="alert">{{ t(checkErrors[item.id]) }}</p>
|
<p v-if="checkErrors[item.id]" role="alert">{{ t(checkErrors[item.id]) }}</p>
|
||||||
<div v-if="checkResults[item.id]" data-json-result role="status">
|
<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.querySelector('[data-json-result]')).toBeNull()
|
||||||
expect(host.textContent).not.toContain('All listed fields are present')
|
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 () => {
|
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)
|
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()
|
const { host } = mount(); host.querySelector<HTMLButtonElement>('[data-evidence-toggle]')!.click(); await flush()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user