mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat: require current managed JSON bindings at goal completion
This commit is contained in:
parent
5cc8765681
commit
4277172529
@ -112,6 +112,31 @@ public class GoalJsonBindingService {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shared completion gate. The held goal lock protects every reference until the status CAS commits. */
|
||||||
|
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||||
|
public List<State> requireForCompletion(vip.mate.goal.model.GoalEntity expected) {
|
||||||
|
var rows = jdbc.query("""
|
||||||
|
SELECT version,evaluation_revision,json_acceptance_required,status FROM mate_agent_goal
|
||||||
|
WHERE id=? AND deleted=0 FOR UPDATE
|
||||||
|
""", (r, i) -> expected.getVersion() != null && r.getLong("version") == expected.getVersion().longValue()
|
||||||
|
&& r.getLong("evaluation_revision") == expected.getEvaluationRevision()
|
||||||
|
&& r.getBoolean("json_acceptance_required")
|
||||||
|
&& Objects.equals(r.getString("status"), expected.getStatus().getValue()), expected.getId());
|
||||||
|
if (rows.size() != 1 || !rows.getFirst()) throw failure("Goal changed before JSON completion; retry with current state");
|
||||||
|
List<State> states = statesLocked(expected.getId());
|
||||||
|
if (states.isEmpty()) throw failure("Required JSON contracts are unavailable");
|
||||||
|
for (State state : states) {
|
||||||
|
if (!state.acceptanceEligible()) throw failure("JSON requirement " + state.criterionKey() + " is not current: " + state.status());
|
||||||
|
}
|
||||||
|
// Recheck expiry after all recipes have run, immediately before returning to the status CAS.
|
||||||
|
Instant now = Instant.now();
|
||||||
|
for (State state : states) {
|
||||||
|
Binding binding = binding(expected.getId(), state.criterionKey());
|
||||||
|
if (binding == null || !binding.expiresAt().isAfter(now)) throw failure("JSON binding expired before completion");
|
||||||
|
}
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
private long evaluationRevision(Long goalId) {
|
private long evaluationRevision(Long goalId) {
|
||||||
Long revision = jdbc.queryForObject("SELECT evaluation_revision FROM mate_agent_goal WHERE id=? AND deleted=0 FOR UPDATE", Long.class, goalId);
|
Long revision = jdbc.queryForObject("SELECT evaluation_revision FROM mate_agent_goal WHERE id=? AND deleted=0 FOR UPDATE", Long.class, goalId);
|
||||||
if (revision == null) throw failure("Goal definition unavailable");
|
if (revision == null) throw failure("Goal definition unavailable");
|
||||||
|
|||||||
@ -72,6 +72,12 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
* effort: memory should never block the state-machine write.
|
* effort: memory should never block the state-machine write.
|
||||||
*/
|
*/
|
||||||
private vip.mate.memory.spi.MemoryManager memoryManager;
|
private vip.mate.memory.spi.MemoryManager memoryManager;
|
||||||
|
private GoalJsonBindingService jsonBindings;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setJsonBindings(GoalJsonBindingService jsonBindings) {
|
||||||
|
this.jsonBindings = jsonBindings;
|
||||||
|
}
|
||||||
|
|
||||||
public GoalServiceImpl(GoalMapper goalMapper,
|
public GoalServiceImpl(GoalMapper goalMapper,
|
||||||
GoalEventMapper eventMapper,
|
GoalEventMapper eventMapper,
|
||||||
@ -390,9 +396,11 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
|
|
||||||
private GoalEntity completeGoal(Long id, GoalEvaluationResult result, boolean evaluated) {
|
private GoalEntity completeGoal(Long id, GoalEvaluationResult result, boolean evaluated) {
|
||||||
boolean[] transitioned = {false};
|
boolean[] transitioned = {false};
|
||||||
|
var jsonProof = new java.util.concurrent.atomic.AtomicReference<List<GoalJsonBindingService.State>>(List.of());
|
||||||
GoalEntity g = retryOptimistic(id, "markCompleted", fresh -> {
|
GoalEntity g = retryOptimistic(id, "markCompleted", fresh -> {
|
||||||
// A failed CAS may retry against another worker's completed row.
|
// A failed CAS may retry against another worker's completed row.
|
||||||
transitioned[0] = false;
|
transitioned[0] = false;
|
||||||
|
jsonProof.set(List.of());
|
||||||
if (fresh.getStatus().isTerminal()) {
|
if (fresh.getStatus().isTerminal()) {
|
||||||
if (evaluated && fresh.getStatus() != GoalStatus.COMPLETED) {
|
if (evaluated && fresh.getStatus() != GoalStatus.COMPLETED) {
|
||||||
throw new MateClawException("err.goal.completion_not_verified", 409,
|
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||||
@ -400,12 +408,6 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
}
|
}
|
||||||
return null; // idempotent
|
return null; // idempotent
|
||||||
}
|
}
|
||||||
if (fresh.isJsonAcceptanceRequired()) {
|
|
||||||
// User-selected requirements never fall back to model text or
|
|
||||||
// the legacy explicit-completion path while bindings are absent.
|
|
||||||
throw new MateClawException("err.goal.json_acceptance_required", 409,
|
|
||||||
"Managed JSON acceptance requires verified current artifact bindings");
|
|
||||||
}
|
|
||||||
if (evaluated && result.evaluationRevision() != fresh.getEvaluationRevision()) {
|
if (evaluated && result.evaluationRevision() != fresh.getEvaluationRevision()) {
|
||||||
throw new MateClawException("err.goal.completion_not_verified", 409,
|
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||||
"Automatic completion requires the current evaluation definition revision");
|
"Automatic completion requires the current evaluation definition revision");
|
||||||
@ -435,6 +437,11 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
.toList();
|
.toList();
|
||||||
w.set(GoalEntity::getCriteria, GoalCriteriaCodec.serialize(allPassed, objectMapper));
|
w.set(GoalEntity::getCriteria, GoalCriteriaCodec.serialize(allPassed, objectMapper));
|
||||||
}
|
}
|
||||||
|
if (fresh.isJsonAcceptanceRequired()) {
|
||||||
|
if (jsonBindings == null) throw new MateClawException("err.goal.json_acceptance_required", 409,
|
||||||
|
"Managed JSON verification service is unavailable");
|
||||||
|
jsonProof.set(jsonBindings.requireForCompletion(fresh));
|
||||||
|
}
|
||||||
bumpVersionAndTime(w);
|
bumpVersionAndTime(w);
|
||||||
transitioned[0] = true;
|
transitioned[0] = true;
|
||||||
return w;
|
return w;
|
||||||
@ -445,6 +452,10 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
detail.put("agentLlmCallsUsed", g.getAgentLlmCallsUsed());
|
detail.put("agentLlmCallsUsed", g.getAgentLlmCallsUsed());
|
||||||
detail.put("evalLlmCallsUsed", g.getEvalLlmCallsUsed());
|
detail.put("evalLlmCallsUsed", g.getEvalLlmCallsUsed());
|
||||||
detail.put("criteria", GoalCriteriaCodec.parse(g.getCriteria(), objectMapper));
|
detail.put("criteria", GoalCriteriaCodec.parse(g.getCriteria(), objectMapper));
|
||||||
|
if (g.isJsonAcceptanceRequired()) {
|
||||||
|
detail.put("jsonAcceptanceRequired", true);
|
||||||
|
detail.put("jsonBindings", jsonProof.get());
|
||||||
|
}
|
||||||
writeEvent(id, GoalEventType.COMPLETED, null, detail);
|
writeEvent(id, GoalEventType.COMPLETED, null, detail);
|
||||||
recordAudit("goal.completed", g, detail);
|
recordAudit("goal.completed", g, detail);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Expand JSON acceptance requirements in the Goal panel. The conversation owner or an administrator explicitly saves up to eight requirements, each mapping an artifact slot to 1–16 top-level fields. Fields must exist and be non-null; false, zero and empty strings are allowed. This is a presence check, not a quality judgment. Opt-in is durable: requirements can be revised using their current revision, but required mode cannot be disabled. Stale revisions produce a conflict.
|
Expand JSON acceptance requirements in the Goal panel. The conversation owner or an administrator explicitly saves up to eight requirements, each mapping an artifact slot to 1–16 top-level fields. Fields must exist and be non-null; false, zero and empty strings are allowed. This is a presence check, not a quality judgment. Opt-in is durable: requirements can be revised using their current revision, but required mode cannot be disabled. Stale revisions produce a conflict.
|
||||||
|
|
||||||
The current stage provides user configuration and independent managed version storage. Binding verification is available; the successful strong-completion path is still pending. Selected goals temporarily reject completion without falling back to textual claims. Unselected goals retain existing behavior.
|
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.
|
||||||
|
|
||||||
## Managed version API
|
## Managed version API
|
||||||
|
|
||||||
@ -29,4 +29,6 @@ Publication and scheduler settlement serialize through the goal lock, rejecting
|
|||||||
|
|
||||||
After publication, call `POST /checks/{criterionKey}` with `expectedRequirementRevision`, `artifactId` and `expectedGeneration`, or use the agent tool `checkManagedGoalJson` with the same fields. Tool revisions and generations are strings. The server checks the specified current slot version using its own fields recipe; it never accepts a caller-provided PASS. `acceptanceEligible=true` applies to that requirement at the time of checking, not to whole-goal completion.
|
After publication, call `POST /checks/{criterionKey}` with `expectedRequirementRevision`, `artifactId` and `expectedGeneration`, or use the agent tool `checkManagedGoalJson` with the same fields. Tool revisions and generations are strings. The server checks the specified current slot version using its own fields recipe; it never accepts a caller-provided PASS. `acceptanceEligible=true` applies to that requirement at the time of checking, not to whole-goal completion.
|
||||||
|
|
||||||
`GET /checks` reads each requirement's current eligibility. Requirement edits, goal-definition edits, a new slot version, expiry or failed body integrity checks invalidate previous bindings. Recheck the current inputs. Binding and goal-version updates share a transaction; rollback cannot leave a passing credential. Historical diagnostic APIs retain `acceptanceEligible=false`; only managed checks create bindings. Integration with the shared completion entry point is still pending.
|
`GET /checks` reads each requirement's current eligibility. Requirement edits, goal-definition edits, a new slot version, expiry or failed body integrity checks invalidate previous bindings. Recheck the current inputs. Binding and goal-version updates share a transaction; rollback cannot leave a passing credential. Historical diagnostic APIs retain `acceptanceEligible=false`; only managed checks create bindings. Completion events retain the accepted requirement revisions, artifact IDs and generations. Transaction rollback emits neither a completion event nor completion memory.
|
||||||
|
|
||||||
|
This is an explicit per-goal managed JSON protocol with a limited scope. The broad execution-evidence ledger retains its existing prerequisites for global ENFORCE. Ordinary tool-success text and diagnostic MATCH results never become bindings automatically. Backend services cover success, invalidation, races and rollback; full browser/service flows, restart and external database validation are still in progress.
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
在 Goal 面板展开“JSON 验收要求”,由对话所有者或管理员显式保存要求。每个 Goal 最多 8 条要求,每条绑定一个产物槽和 1–16 个顶层字段。字段检查表示字段存在且不为 null;false、0 和空字符串允许,不等于内容质量判断。保存后不可关闭强验收模式,可以带当前 revision 修改要求;旧修订会返回冲突。
|
在 Goal 面板展开“JSON 验收要求”,由对话所有者或管理员显式保存要求。每个 Goal 最多 8 条要求,每条绑定一个产物槽和 1–16 个顶层字段。字段检查表示字段存在且不为 null;false、0 和空字符串允许,不等于内容质量判断。保存后不可关闭强验收模式,可以带当前 revision 修改要求;旧修订会返回冲突。
|
||||||
|
|
||||||
当前阶段已提供用户配置与独立受管版本存储;绑定检查已接通,强验收成功完成路径尚未接通。选中此模式的 Goal 暂时拒绝完成,不会回退到文字声明;未选中的 Goal 保持既有行为。
|
当前已接通用户配置、独立受管版本、绑定检查及共享完成检查。选中模式后,所有当前要求必须具有匹配的有效绑定,才可在既有完成规则满足时完成;自动评估、显式 completeGoal 和重试均使用同一完成检查。未选中的 Goal 保持既有行为。
|
||||||
|
|
||||||
## 受管版本接口
|
## 受管版本接口
|
||||||
|
|
||||||
@ -29,4 +29,6 @@
|
|||||||
|
|
||||||
发布后,调用 `POST /checks/{criterionKey}`,提交 `expectedRequirementRevision`、`artifactId`、`expectedGeneration`;代理使用 `checkManagedGoalJson` 传相同字段。所有修订和 generation 在代理工具里都是字符串。服务端只检查当前槽的指定版本,运行自己的字段 recipe,不接受调用方提供的 PASS。返回 `acceptanceEligible=true` 表示这条要求当前匹配,不能代表整个 Goal 已完成。
|
发布后,调用 `POST /checks/{criterionKey}`,提交 `expectedRequirementRevision`、`artifactId`、`expectedGeneration`;代理使用 `checkManagedGoalJson` 传相同字段。所有修订和 generation 在代理工具里都是字符串。服务端只检查当前槽的指定版本,运行自己的字段 recipe,不接受调用方提供的 PASS。返回 `acceptanceEligible=true` 表示这条要求当前匹配,不能代表整个 Goal 已完成。
|
||||||
|
|
||||||
`GET /checks` 读取每条要求的当前资格。要求修改、Goal 定义修改、槽出现新版本、版本过期或正文完整性失败都会使旧绑定失效;需要按当前条件重新检查。每次绑定与 Goal version 更新同事务,失败回滚不留下通过凭据。历史诊断接口的 `acceptanceEligible=false` 保持不变,只有此受管版本检查产生绑定。当前统一完成入口仍在接入中。
|
`GET /checks` 读取每条要求的当前资格。要求修改、Goal 定义修改、槽出现新版本、版本过期或正文完整性失败都会使旧绑定失效;需要按当前条件重新检查。每次绑定与 Goal version 更新同事务,失败回滚不留下通过凭据。历史诊断接口的 `acceptanceEligible=false` 保持不变,只有此受管版本检查产生绑定。完成事件保留本次受管绑定的条件修订、产物 ID 和 generation 引用;事务回滚不发布完成事件或完成记忆。
|
||||||
|
|
||||||
|
这是逐 Goal 显式选择的有限受管 JSON 协议;宽泛执行证据账本的全局 ENFORCE 配置仍遵循原有准入限制。此协议不把任何普通工具成功文本或诊断 MATCH 自动升级为绑定。后端服务已覆盖成功、失效、竞争和回滚;完整浏览器服务闭环、重启和外部数据库验证仍在推进。
|
||||||
|
|||||||
@ -442,4 +442,116 @@ class GoalJsonAcceptanceIntegrationTest {
|
|||||||
assertFalse(bindings.state(goal.getId(), alice).getFirst().acceptanceEligible());
|
assertFalse(bindings.state(goal.getId(), alice).getFirst().acceptanceEligible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@org.junit.jupiter.params.provider.CsvSource({"false,false", "false,true", "true,false", "true,true"})
|
||||||
|
void currentManagedBindingsPermitBothCompletionPaths(boolean persistent, boolean automatic) {
|
||||||
|
GoalEntity goal = goal(persistent);
|
||||||
|
goals.appendCriterion(goal.getId(), "Produce the report", alice);
|
||||||
|
var evaluation = new GoalEvaluationResult(1, "report checked", "completed", true, "fixture", 1, 0,
|
||||||
|
List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "fixture semantic verdict")), null);
|
||||||
|
goals.recordEvaluation(goal.getId(), evaluation, 1, 1);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
GoalEntity completed = assertDoesNotThrow(() -> automatic
|
||||||
|
? goals.markEvaluatedCompleted(goal.getId(), evaluation) : goals.markCompleted(goal.getId(), evaluation));
|
||||||
|
assertEquals(GoalStatus.COMPLETED, completed.getStatus());
|
||||||
|
assertEquals(1, goals.listEvents(goal.getId(), 30).stream().filter(e -> "completed".equals(e.getEventType())).count());
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), evaluation).getStatus());
|
||||||
|
assertEquals(1, goals.listEvents(goal.getId(), 30).stream().filter(e -> "completed".equals(e.getEventType())).count());
|
||||||
|
assertThrows(MateClawException.class, () -> artifacts.publish(goal.getId(), "report", publication(1, "{}"), alice));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void allCurrentRequirementsMustBindBeforeCompletion() {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
acceptance.configure(goal.getId(), "s", new GoalJsonAcceptanceService.ConfigureRequest(0L, "sources", List.of("items")), alice);
|
||||||
|
var report = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, report), alice);
|
||||||
|
assertThrows(MateClawException.class, () -> goals.markCompleted(goal.getId(), null));
|
||||||
|
var sources = artifacts.publish(goal.getId(), "sources", publication(0, "{\"items\":[]}"), alice);
|
||||||
|
assertThrows(MateClawException.class, () -> goals.markCompleted(goal.getId(), null));
|
||||||
|
bindings.check(goal.getId(), "s", checkRequest(1, sources), alice);
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), null).getStatus());
|
||||||
|
String proof = goals.listEvents(goal.getId(), 30).stream().filter(e -> "completed".equals(e.getEventType())).findFirst().orElseThrow().getDetailJson();
|
||||||
|
assertTrue(proof.contains(report.artifactId())); assertTrue(proof.contains(sources.artifactId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void completionRejectsEveryInvalidationAndFreshBindingRestoresSuccess() {
|
||||||
|
for (String invalidation : List.of("requirement", "definition", "superseded", "expired", "corrupt")) {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true,\"sources\":[]}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
long revision = 1;
|
||||||
|
switch (invalidation) {
|
||||||
|
case "requirement" -> { acceptance.configure(goal.getId(), "r", request(1, "summary", "sources"), alice); revision = 2; }
|
||||||
|
case "definition" -> { GoalUpdateRequest edit = new GoalUpdateRequest(); edit.setDescription("new definition"); goals.update(goal.getId(), edit, alice); }
|
||||||
|
case "superseded" -> version = artifacts.publish(goal.getId(), "report", publication(1, "{\"summary\":true}"), alice);
|
||||||
|
case "expired" -> jdbc.update("UPDATE mate_goal_json_artifact SET expires_at=? WHERE artifact_id=?", java.sql.Timestamp.from(java.time.Instant.now().minusSeconds(1)), version.artifactId());
|
||||||
|
case "corrupt" -> jdbc.update("UPDATE mate_goal_json_artifact SET json_body='{}' WHERE artifact_id=?", version.artifactId());
|
||||||
|
}
|
||||||
|
assertThrows(MateClawException.class, () -> goals.markCompleted(goal.getId(), null), invalidation);
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
if (List.of("expired", "corrupt").contains(invalidation)) version = artifacts.publish(goal.getId(), "report", publication(1, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(revision, version), alice);
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), null).getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void completionRollbackPreservesActiveGoalAndDoesNotEmitSuccessOrMemory() {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
new TransactionTemplate(transactions).executeWithoutResult(status -> {
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), null).getStatus());
|
||||||
|
status.setRollbackOnly();
|
||||||
|
});
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
assertTrue(goals.listEvents(goal.getId(), 30).stream().noneMatch(e -> "completed".equals(e.getEventType())));
|
||||||
|
org.mockito.Mockito.verify(memory, org.mockito.Mockito.never()).syncAll(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), null).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void newPublicationAndCompletionCannotBothWinUsingAnOldBinding() throws Exception {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
var start = new java.util.concurrent.CountDownLatch(1);
|
||||||
|
try (var workers = java.util.concurrent.Executors.newFixedThreadPool(2)) {
|
||||||
|
var publish = workers.submit(() -> {
|
||||||
|
start.await();
|
||||||
|
try { artifacts.publish(goal.getId(), "report", publication(1, "{}"), alice); return true; }
|
||||||
|
catch (MateClawException terminal) { return false; }
|
||||||
|
});
|
||||||
|
var complete = workers.submit(() -> {
|
||||||
|
start.await();
|
||||||
|
try { goals.markCompleted(goal.getId(), null); return true; }
|
||||||
|
catch (MateClawException stale) { return false; }
|
||||||
|
});
|
||||||
|
start.countDown();
|
||||||
|
boolean published = publish.get(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
boolean completed = complete.get(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
assertNotEquals(published, completed);
|
||||||
|
assertEquals(completed ? GoalStatus.COMPLETED : GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
assertEquals(published ? 2 : 1, artifacts.list(goal.getId(), alice).getFirst().generation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void actualExplicitCompletionToolCannotBypassBindingsButCanCompleteAfterCheck() {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var properties = new vip.mate.goal.config.GoalProperties(); properties.setEnabled(true);
|
||||||
|
var tool = new vip.mate.tool.builtin.GoalManagementTool(goals, properties, new com.fasterxml.jackson.databind.ObjectMapper(), null);
|
||||||
|
var context = accountOrigin(goal, alice).toToolContext();
|
||||||
|
assertTrue(tool.completeGoal(context).contains("error"));
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
assertTrue(tool.completeGoal(context).contains("\"status\":\"completed\""));
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user