mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
fix(goal): refresh terminal status in JSON requirements panel
This commit is contained in:
parent
770f4e16d5
commit
ab1f5069ad
@ -25,13 +25,13 @@ public class GoalJsonAcceptanceService {
|
||||
public record ConfigureRequest(Long expectedRevision, String artifactSlot, List<String> requiredFields) { }
|
||||
public record Requirement(String criterionKey, String artifactSlot, long revision,
|
||||
List<String> requiredFields, String configuredBy) { }
|
||||
public record View(boolean required, List<Requirement> requirements) { }
|
||||
public record View(boolean required, String status, List<Requirement> requirements) { }
|
||||
record GoalScope(long id, String conversationId, long workspaceId, long agentId, String status, boolean required) { }
|
||||
|
||||
@Transactional
|
||||
public View get(Long goalId, String username) {
|
||||
GoalScope goal = authorizedGoal(goalId, username, true);
|
||||
return new View(goal.required(), requirements(goalId));
|
||||
return new View(goal.required(), goal.status(), requirements(goalId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@ -4,13 +4,13 @@ Open a conversation with an existing Goal, click the Goals button in its header,
|
||||
|
||||
User configuration, independent managed versions, binding checks and the shared completion gate are connected. Every current requirement needs a matching valid binding before a selected goal can complete under its existing completion rules. Automatic evaluation, explicit completeGoal and retries share that gate. Unselected goals retain existing behavior.
|
||||
|
||||
The conversation Goals panel includes paused and terminal goals, loading 20 at a time with an option to load older records. Paused goals still allow requirement edits, publication and checks; terminal goals only expose existing requirements and content. Closing the panel, switching conversations or leaving the page clears its contents, and a failed refresh clears the old list. Reading history does not resume execution.
|
||||
The conversation Goals panel includes paused and terminal goals, loading 20 at a time with an option to load older records. Paused goals still allow requirement edits, publication and checks; terminal goals only expose existing requirements and content. Closing the panel, switching conversations or leaving the page clears its contents, and a failed refresh clears the old list. Reading history does not resume execution. Reloading requirements also refreshes the Goal status, so a goal completed since the list was loaded becomes read-only in the acceptance panel.
|
||||
|
||||
## Managed version API
|
||||
|
||||
Prefix: `/api/v1/goals/{goalId}/json-acceptance`. An enabled account with conversation-owner or administrator permission is required. Preserve IDs, revisions and generations as strings in clients.
|
||||
|
||||
- `GET /`: read required mode and requirements.
|
||||
- `GET /`: read required mode, current Goal status and requirements.
|
||||
- `PUT /requirements/{criterionKey}`: send `expectedRevision`, `artifactSlot` and `requiredFields`; use revision `0` for a new requirement.
|
||||
- `GET /artifacts`: list required slots and current versions; an empty slot has generation `0`.
|
||||
- `POST /artifacts/{slot}`: send `expectedGeneration` and `jsonContent` (a string containing the original JSON body) to append a version and atomically advance the slot.
|
||||
|
||||
@ -4,13 +4,13 @@
|
||||
|
||||
当前已接通用户配置、独立受管版本、绑定检查及共享完成检查。选中模式后,所有当前要求必须具有匹配的有效绑定,才可在既有完成规则满足时完成;自动评估、显式 completeGoal 和重试均使用同一完成检查。未选中的 Goal 保持既有行为。
|
||||
|
||||
当前会话的“目标”面板也保留暂停、已完成等历史目标,每次读取20项,可加载更早记录。暂停目标仍可修订要求、发布和检查;终态目标只能查看要求与已有正文。关闭面板、切换会话或离开页面后清除面板内容;刷新失败时清除旧列表。读取历史不会恢复目标运行。
|
||||
当前会话的“目标”面板也保留暂停、已完成等历史目标,每次读取20项,可加载更早记录。暂停目标仍可修订要求、发布和检查;终态目标只能查看要求与已有正文。关闭面板、切换会话或离开页面后清除面板内容;刷新失败时清除旧列表。读取历史不会恢复目标运行。刷新要求也会刷新 Goal 状态,因此列表加载后才完成的目标会在验收面板切换为只读。
|
||||
|
||||
## 受管版本接口
|
||||
|
||||
接口前缀 `/api/v1/goals/{goalId}/json-acceptance`,需要启用账户及对话所有者或管理员权限。ID、revision 和 generation 在响应中使用字符串,客户端应原样保留。
|
||||
|
||||
- `GET /`:读取启用状态和要求。
|
||||
- `GET /`:读取启用状态、Goal 当前状态和要求。
|
||||
- `PUT /requirements/{criterionKey}`:提交 `expectedRevision`、`artifactSlot`、`requiredFields`。新要求的 revision 为 `0`。
|
||||
- `GET /artifacts`:列出当前要求使用的槽及当前版本。空槽 generation 为 `0`。
|
||||
- `POST /artifacts/{slot}`:提交 `expectedGeneration` 和 `jsonContent`(包含原始 JSON 正文的字符串),原子追加新版本并推进槽。
|
||||
|
||||
@ -248,6 +248,8 @@ class GoalJsonHttpRuntimeIntegrationTest {
|
||||
assertTrue(calls.get() >= (accepted ? 6 : 2) && calls.get() <= (accepted ? 10 : 4), "Bounded offline model calls: " + calls.get());
|
||||
if (!accepted) assertEquals(recovered ? 1 : 0,
|
||||
jdbc.queryForObject("SELECT COUNT(*) FROM mate_goal_json_artifact WHERE goal_id=?", Integer.class, goal.getId()));
|
||||
JsonNode currentRequirements = request("GET", "/api/v1/goals/" + goal.getId() + "/json-acceptance", token, null);
|
||||
assertEquals(accepted ? "completed" : "active", currentRequirements.path("data").path("status").asText());
|
||||
verify(modelFactory, atLeastOnce()).buildFor(any(), any());
|
||||
}
|
||||
|
||||
@ -273,7 +275,7 @@ class GoalJsonHttpRuntimeIntegrationTest {
|
||||
.timeout(Duration.ofSeconds(45)).header("Content-Type", "application/json").header("X-Workspace-Id", "1");
|
||||
if (token != null) builder.header("Authorization", "Bearer " + token);
|
||||
var response = HttpClient.newHttpClient().send(builder.method(method,
|
||||
HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body))).build(), HttpResponse.BodyHandlers.ofString());
|
||||
(body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body)))).build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode(), response.body());
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ export interface GoalJsonRequirement {
|
||||
requiredFields: string[]
|
||||
configuredBy: string
|
||||
}
|
||||
export interface GoalJsonAcceptanceView { required: boolean; requirements: GoalJsonRequirement[] }
|
||||
export interface GoalJsonAcceptanceView { required: boolean; status: string; requirements: GoalJsonRequirement[] }
|
||||
export interface ConfigureJsonRequirement { expectedRevision: string; artifactSlot: string; requiredFields: string[] }
|
||||
export interface ManagedJsonArtifact {
|
||||
artifactId: string; artifactSlot: string; generation: string; sha256: string; byteLength: number
|
||||
|
||||
@ -17,7 +17,8 @@ const slot = ref('')
|
||||
const fields = ref('')
|
||||
const revision = ref('0')
|
||||
let generation = 0
|
||||
const editable = computed(() => ['active', 'paused'].includes(props.status))
|
||||
const currentStatus = computed(() => ['active', 'paused'].includes(props.status) ? view.value?.status : props.status)
|
||||
const editable = computed(() => !!view.value && ['active', 'paused'].includes(currentStatus.value ?? ''))
|
||||
const busy = computed(() => loading.value || saving.value)
|
||||
|
||||
function resetForm() { key.value = ''; slot.value = ''; fields.value = ''; revision.value = '0'; conflict.value = false }
|
||||
@ -76,7 +77,7 @@ async function save() {
|
||||
expectedRevision: revision.value, artifactSlot: slot.value, requiredFields: required,
|
||||
})
|
||||
if (request !== generation || !view.value) return
|
||||
view.value = { required: true, requirements: [...view.value.requirements.filter(r => r.criterionKey !== data.criterionKey), data]
|
||||
view.value = { required: true, status: view.value.status, requirements: [...view.value.requirements.filter(r => r.criterionKey !== data.criterionKey), data]
|
||||
.sort((a, b) => a.criterionKey.localeCompare(b.criterionKey)) }
|
||||
resetForm()
|
||||
} catch (failure) {
|
||||
@ -105,6 +106,7 @@ onBeforeUnmount(() => { generation++ })
|
||||
<p v-if="error" role="alert">{{ t(error) }}</p>
|
||||
<p v-if="loading" role="status">{{ t('common.loading') }}</p>
|
||||
<template v-if="view">
|
||||
<p v-if="currentStatus" data-json-acceptance-status>{{ t('goalJsonAcceptance.historyStatus.' + currentStatus) }}</p>
|
||||
<p data-json-acceptance-mode>{{ t(view.required ? 'goalJsonAcceptance.required' : 'goalJsonAcceptance.notSelected') }}</p>
|
||||
<ul v-if="view.requirements.length">
|
||||
<li v-for="requirement in view.requirements" :key="requirement.criterionKey" data-json-requirement>
|
||||
@ -113,7 +115,7 @@ onBeforeUnmount(() => { generation++ })
|
||||
<button v-if="editable" type="button" :disabled="busy" data-json-requirement-edit @click="edit(requirement)">{{ t('goalJsonAcceptance.edit') }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ManagedGoalJsonVersions v-if="view.required" :goal-id="goalId" :status="status" :requirements="view.requirements" @access-lost="showFailure({ code: 403 })" />
|
||||
<ManagedGoalJsonVersions v-if="view.required" :goal-id="goalId" :status="currentStatus || status" :requirements="view.requirements" @access-lost="showFailure({ code: 403 })" />
|
||||
<form v-if="editable" @submit.prevent="save">
|
||||
<p>{{ t('goalJsonAcceptance.selectionNotice') }}</p>
|
||||
<label>{{ t('goalJsonAcceptance.key') }}<input v-model="key" data-json-requirement-key required maxlength="64" :disabled="busy || revision !== '0'" placeholder="report-fields" /></label>
|
||||
|
||||
@ -25,7 +25,7 @@ async function submit(host: HTMLElement) { host.querySelector('form')!.dispatchE
|
||||
afterEach(() => { apps.splice(0).forEach(app => app.unmount()); document.body.innerHTML = ''; vi.resetAllMocks() })
|
||||
describe('user JSON acceptance requirements', () => {
|
||||
it('loads without writing and requires an explicit save to opt in', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: false, requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: false, status: 'active', requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockResolvedValue({ data: requirement() })
|
||||
const { host } = mount(); expect(goalJsonAcceptanceApi.get).not.toHaveBeenCalled(); await open(host)
|
||||
expect(goalJsonAcceptanceApi.configure).not.toHaveBeenCalled()
|
||||
@ -39,7 +39,7 @@ describe('user JSON acceptance requirements', () => {
|
||||
})
|
||||
it('preserves opaque revisions and refuses to resubmit a stale edit before reload', async () => {
|
||||
const revision = '9223372036854775802'
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement(revision)] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, status: 'active', requirements: [requirement(revision)] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockRejectedValue({ code: 409 })
|
||||
const { host } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush()
|
||||
@ -49,8 +49,23 @@ describe('user JSON acceptance requirements', () => {
|
||||
expect(host.querySelector<HTMLButtonElement>('[data-json-requirement-save]')!.disabled).toBe(true)
|
||||
await submit(host); expect(goalJsonAcceptanceApi.configure).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('reloads a server-completed goal as read-only even while the parent status is stale', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get)
|
||||
.mockResolvedValueOnce({ data: { required: true, status: 'active', requirements: [requirement()] } })
|
||||
.mockResolvedValueOnce({ data: { required: true, status: 'completed', requirements: [requirement()] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockRejectedValue({ code: 409 })
|
||||
const { host, props } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush(); await submit(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-acceptance-refresh]')!.click(); await flush()
|
||||
expect(props.status).toBe('active')
|
||||
expect(host.querySelectorAll('[data-json-requirement]')).toHaveLength(1)
|
||||
expect(host.querySelector('form')).toBeNull()
|
||||
expect(host.querySelector('[data-json-requirement-edit]')).toBeNull()
|
||||
expect(host.querySelector('[data-json-acceptance-status]')?.textContent).toContain('Completed')
|
||||
expect(goalJsonAcceptanceApi.configure).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('clears old requirements and drafts after access is revoked', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement()] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, status: 'active', requirements: [requirement()] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockRejectedValue({ code: 403 })
|
||||
const { host } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush(); await submit(host)
|
||||
@ -61,16 +76,16 @@ describe('user JSON acceptance requirements', () => {
|
||||
it('does not show an old goal read response after selection changes', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockImplementationOnce(() => new Promise(r => { resolve = r }) as never)
|
||||
.mockResolvedValueOnce({ data: { required: false, requirements: [] } })
|
||||
.mockResolvedValueOnce({ data: { required: false, status: 'active', requirements: [] } })
|
||||
const { host, props } = mount(); await open(host); props.goalId = 'new'; await flush()
|
||||
resolve({ data: { required: true, requirements: [requirement()] } }); await flush()
|
||||
resolve({ data: { required: true, status: 'active', requirements: [requirement()] } }); await flush()
|
||||
expect(host.querySelectorAll('[data-json-requirement]')).toHaveLength(0)
|
||||
expect(host.querySelector('[data-json-acceptance-mode]')?.textContent).toContain('has not selected')
|
||||
})
|
||||
it('does not apply a delayed save to a different goal', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValueOnce({ data: { required: true, requirements: [requirement()] } })
|
||||
.mockResolvedValueOnce({ data: { required: false, requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValueOnce({ data: { required: true, status: 'active', requirements: [requirement()] } })
|
||||
.mockResolvedValueOnce({ data: { required: false, status: 'active', requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockImplementationOnce(() => new Promise(r => { resolve = r }) as never)
|
||||
const { host, props } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush(); await submit(host)
|
||||
@ -79,7 +94,7 @@ describe('user JSON acceptance requirements', () => {
|
||||
expect(host.querySelector('[data-json-acceptance-mode]')?.textContent).toContain('has not selected')
|
||||
})
|
||||
it('does not submit duplicate fields and presents terminal goals as read-only', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement()] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, status: 'active', requirements: [requirement()] } })
|
||||
const { host, props } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush()
|
||||
await fill(host, '[data-json-requirement-fields]', 'summary\nsummary'); await submit(host)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user