mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
Persist user JSON acceptance contracts and block unverified completion
This commit is contained in:
parent
2c73da7e2a
commit
868e2dbfb1
@ -0,0 +1,30 @@
|
||||
package vip.mate.goal.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.goal.service.GoalJsonAcceptanceService;
|
||||
|
||||
/** Authenticated user surface, intentionally not a model tool. */
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/goals/{goalId}/json-acceptance")
|
||||
@RequiredArgsConstructor
|
||||
public class GoalJsonAcceptanceController {
|
||||
private final GoalJsonAcceptanceService acceptance;
|
||||
|
||||
@GetMapping
|
||||
public R<GoalJsonAcceptanceService.View> get(@PathVariable Long goalId, Authentication auth) {
|
||||
return R.ok(acceptance.get(goalId, username(auth)));
|
||||
}
|
||||
|
||||
@PutMapping("/requirements/{criterionKey}")
|
||||
public R<GoalJsonAcceptanceService.Requirement> configure(@PathVariable Long goalId, @PathVariable String criterionKey,
|
||||
@RequestBody GoalJsonAcceptanceService.ConfigureRequest request, Authentication auth) {
|
||||
return R.ok(acceptance.configure(goalId, criterionKey, request, username(auth)));
|
||||
}
|
||||
|
||||
private static String username(Authentication auth) {
|
||||
return auth != null && auth.isAuthenticated() ? auth.getName() : null;
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,9 @@ public class GoalEntity {
|
||||
/** Advances on evaluation-definition edits, independently of optimistic-lock/usage version. */
|
||||
private long evaluationRevision;
|
||||
|
||||
/** User-selected managed JSON acceptance; never falls back to semantic completion. */
|
||||
private boolean jsonAcceptanceRequired;
|
||||
|
||||
/** LLM-readable exit criteria; evaluator scores against this. Nullable. */
|
||||
@TableField(value = "exit_criteria", updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String exitCriteria;
|
||||
|
||||
@ -30,6 +30,8 @@ public class GoalResponse {
|
||||
|
||||
private GoalStatus status;
|
||||
|
||||
private boolean jsonAcceptanceRequired;
|
||||
|
||||
/** Opts into durable continuation; zero budgets mean unlimited only in this mode. */
|
||||
private Boolean persistentExecution;
|
||||
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
package vip.mate.goal.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.execution.evidence.service.JsonArtifactRecipe;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** User-managed requirements. Agent tools must not expose this configuration surface. */
|
||||
@Service
|
||||
public class GoalJsonAcceptanceService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper json;
|
||||
|
||||
public GoalJsonAcceptanceService(JdbcTemplate jdbc, ObjectMapper json) {
|
||||
this.jdbc = jdbc;
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
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) { }
|
||||
private record GoalScope(long id, String conversationId, long workspaceId, 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));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Requirement configure(Long goalId, String criterionKey, ConfigureRequest request, String username) {
|
||||
GoalScope goal = authorizedGoal(goalId, username, true);
|
||||
if (!List.of("active", "paused").contains(goal.status())) {
|
||||
throw failure(409, "JSON requirements can only change on an active or paused goal");
|
||||
}
|
||||
if (request == null || request.expectedRevision() == null || request.expectedRevision() < 0) {
|
||||
throw failure(400, "expectedRevision is required (0 for a new requirement)");
|
||||
}
|
||||
validateKey(criterionKey);
|
||||
validateKey(request.artifactSlot());
|
||||
List<String> fields = JsonArtifactRecipe.validate(request.requiredFields());
|
||||
List<Requirement> current = requirements(goalId);
|
||||
Requirement previous = current.stream().filter(r -> r.criterionKey().equals(criterionKey)).findFirst().orElse(null);
|
||||
long revision = previous == null ? 0 : previous.revision();
|
||||
if (request.expectedRevision() != revision) throw failure(409, "JSON requirement revision changed; reload before editing");
|
||||
if (previous != null && previous.artifactSlot().equals(request.artifactSlot()) && previous.requiredFields().equals(fields)) {
|
||||
return previous;
|
||||
}
|
||||
if (previous == null && current.size() >= 8) throw failure(400, "At most 8 JSON requirements per goal");
|
||||
long next = Math.addExact(revision, 1);
|
||||
String encoded = encode(fields);
|
||||
if (previous == null) {
|
||||
jdbc.update("""
|
||||
INSERT INTO mate_goal_json_requirement
|
||||
(goal_id,criterion_key,artifact_slot,revision,required_fields,created_by,updated_by,created_at,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
|
||||
""", goalId, criterionKey, request.artifactSlot(), next, encoded, username, username);
|
||||
} else {
|
||||
jdbc.update("""
|
||||
UPDATE mate_goal_json_requirement SET artifact_slot=?,revision=?,required_fields=?,updated_by=?,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE goal_id=? AND criterion_key=?
|
||||
""", request.artifactSlot(), next, encoded, username, goalId, criterionKey);
|
||||
}
|
||||
// This version write races safely with existing GoalService CAS completion.
|
||||
// No removal/disable endpoint: opting in never silently restores text-only completion.
|
||||
jdbc.update("UPDATE mate_agent_goal SET json_acceptance_required=TRUE,version=version+1,update_time=CURRENT_TIMESTAMP WHERE id=?", goalId);
|
||||
return new Requirement(criterionKey, request.artifactSlot(), next, fields, username);
|
||||
}
|
||||
|
||||
private GoalScope authorizedGoal(Long goalId, String username, boolean lock) {
|
||||
if (username == null || username.isBlank() || "anonymous".equals(username)) throw failure(401, "Authentication required");
|
||||
// Deliberately stricter than legacy system-conversation ownership fallback.
|
||||
List<String> roles = jdbc.queryForList("SELECT role FROM mate_user WHERE username=? AND enabled=TRUE AND deleted=0" + (lock ? " FOR UPDATE" : ""), String.class, username);
|
||||
if (roles.size() != 1) throw failure(403, "An enabled user account is required");
|
||||
GoalScope initial = goal(goalId, false);
|
||||
var conversations = jdbc.query("SELECT username,workspace_id FROM mate_conversation WHERE conversation_id=? AND deleted=0" + (lock ? " FOR UPDATE" : ""),
|
||||
(row, i) -> Objects.equals(row.getString("username"), username) || "admin".equalsIgnoreCase(roles.getFirst())
|
||||
? row.getLong("workspace_id") : null, initial.conversationId());
|
||||
if (conversations.size() != 1 || !Objects.equals(conversations.getFirst(), initial.workspaceId())) throw failure(403, "Goal owner permission required");
|
||||
GoalScope current = lock ? goal(goalId, true) : initial;
|
||||
if (!Objects.equals(current.conversationId(), initial.conversationId()) || current.workspaceId() != initial.workspaceId()) {
|
||||
throw failure(409, "Goal scope changed; reload before editing");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private GoalScope goal(Long id, boolean lock) {
|
||||
var rows = jdbc.query("SELECT id,conversation_id,workspace_id,status,json_acceptance_required FROM mate_agent_goal WHERE id=? AND deleted=0" + (lock ? " FOR UPDATE" : ""),
|
||||
(row, i) -> new GoalScope(row.getLong("id"), row.getString("conversation_id"), row.getLong("workspace_id"), row.getString("status"), row.getBoolean("json_acceptance_required")), id);
|
||||
if (rows.size() != 1) throw failure(404, "Goal not found");
|
||||
return rows.getFirst();
|
||||
}
|
||||
|
||||
private List<Requirement> requirements(Long goalId) {
|
||||
return jdbc.query("SELECT criterion_key,artifact_slot,revision,required_fields,updated_by FROM mate_goal_json_requirement WHERE goal_id=? ORDER BY criterion_key",
|
||||
(row, i) -> new Requirement(row.getString("criterion_key"), row.getString("artifact_slot"), row.getLong("revision"), decode(row.getString("required_fields")), row.getString("updated_by")), goalId);
|
||||
}
|
||||
|
||||
private static void validateKey(String key) {
|
||||
if (key == null || !key.matches("[a-z][a-z0-9_-]{0,63}")) throw failure(400, "Keys must be 1–64 lowercase letters, digits, underscores or hyphens, starting with a letter");
|
||||
}
|
||||
|
||||
private String encode(List<String> fields) {
|
||||
try { return json.writeValueAsString(fields); }
|
||||
catch (Exception e) { throw new IllegalStateException("Cannot encode JSON requirements", e); }
|
||||
}
|
||||
|
||||
private List<String> decode(String fields) {
|
||||
try { return JsonArtifactRecipe.validate(json.readValue(fields, new TypeReference<List<String>>() { })); }
|
||||
catch (Exception e) { throw new IllegalStateException("Stored JSON requirements are invalid", e); }
|
||||
}
|
||||
|
||||
private static MateClawException failure(int code, String message) { return new MateClawException(code, message); }
|
||||
}
|
||||
@ -400,6 +400,12 @@ public class GoalServiceImpl implements GoalService {
|
||||
}
|
||||
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()) {
|
||||
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||
"Automatic completion requires the current evaluation definition revision");
|
||||
@ -737,6 +743,7 @@ public class GoalServiceImpl implements GoalService {
|
||||
r.setExitCriteria(e.getExitCriteria());
|
||||
r.setSuccessCheckPrompt(e.getSuccessCheckPrompt());
|
||||
r.setStatus(e.getStatus());
|
||||
r.setJsonAcceptanceRequired(e.isJsonAcceptanceRequired());
|
||||
r.setPersistentExecution(Boolean.TRUE.equals(e.getPersistentExecution()));
|
||||
r.setTurnBudget(e.getTurnBudget());
|
||||
r.setTurnsUsed(e.getTurnsUsed());
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
-- Explicit opt-in is durable and cannot silently fall back to text completion.
|
||||
ALTER TABLE mate_agent_goal ADD COLUMN json_acceptance_required BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
CREATE TABLE mate_goal_json_requirement (
|
||||
goal_id BIGINT NOT NULL,
|
||||
criterion_key VARCHAR(64) NOT NULL,
|
||||
artifact_slot VARCHAR(64) NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
required_fields TEXT NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
updated_by VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (goal_id, criterion_key)
|
||||
);
|
||||
@ -0,0 +1,14 @@
|
||||
-- Explicit opt-in is durable and cannot silently fall back to text completion.
|
||||
ALTER TABLE mate_agent_goal ADD COLUMN json_acceptance_required BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
CREATE TABLE mate_goal_json_requirement (
|
||||
goal_id BIGINT NOT NULL,
|
||||
criterion_key VARCHAR(64) NOT NULL,
|
||||
artifact_slot VARCHAR(64) NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
required_fields TEXT NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
updated_by VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (goal_id, criterion_key)
|
||||
);
|
||||
@ -0,0 +1,14 @@
|
||||
-- Explicit opt-in is durable and cannot silently fall back to text completion.
|
||||
ALTER TABLE mate_agent_goal ADD COLUMN json_acceptance_required BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
CREATE TABLE mate_goal_json_requirement (
|
||||
goal_id BIGINT NOT NULL,
|
||||
criterion_key VARCHAR(64) NOT NULL,
|
||||
artifact_slot VARCHAR(64) NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
required_fields TEXT NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
updated_by VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (goal_id, criterion_key)
|
||||
);
|
||||
@ -0,0 +1,156 @@
|
||||
package vip.mate.goal;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import vip.mate.MateClawApplication;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.goal.model.*;
|
||||
import vip.mate.goal.service.GoalJsonAcceptanceService;
|
||||
import vip.mate.goal.service.GoalService;
|
||||
import vip.mate.memory.spi.MemoryManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(classes = MateClawApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:json_acceptance_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
|
||||
"spring.ai.dashscope.api-key=test-key", "spring.main.web-application-type=none",
|
||||
"mateclaw.goal.enabled=false", "mateclaw.plugin.enabled=false", "mateclaw.skill.workspace.auto-init=false",
|
||||
"mateclaw.skill.workspace.root=${java.io.tmpdir}/mateclaw-json-acceptance-skills-${random.uuid}"
|
||||
})
|
||||
class GoalJsonAcceptanceIntegrationTest {
|
||||
@MockBean private MemoryManager memory;
|
||||
@Autowired private GoalService goals;
|
||||
@Autowired private GoalJsonAcceptanceService acceptance;
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
@Autowired private PlatformTransactionManager transactions;
|
||||
private String alice;
|
||||
private String bob;
|
||||
|
||||
@BeforeEach void users() {
|
||||
alice = "alice-" + UUID.randomUUID();
|
||||
bob = "bob-" + UUID.randomUUID();
|
||||
for (String user : List.of(alice, bob)) jdbc.update("""
|
||||
INSERT INTO mate_user(id,username,password,enabled,role,create_time,update_time,deleted)
|
||||
VALUES (?,?,?,TRUE,'user',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)
|
||||
""", IdWorker.getId(), user, "unused-test-password");
|
||||
}
|
||||
|
||||
private GoalEntity goal(boolean persistent) {
|
||||
String conversation = UUID.randomUUID().toString();
|
||||
jdbc.update("""
|
||||
INSERT INTO mate_conversation(id,conversation_id,username,workspace_id,agent_id,create_time,update_time,deleted)
|
||||
VALUES (?,?,?,1,1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)
|
||||
""", IdWorker.getId(), conversation, alice);
|
||||
GoalCreateRequest req = new GoalCreateRequest();
|
||||
req.setConversationId(conversation); req.setWorkspaceId(1L); req.setAgentId(1L);
|
||||
req.setTitle("A JSON report"); req.setDescription("Produce a managed JSON report"); req.setPersistentExecution(persistent);
|
||||
return goals.create(req, alice);
|
||||
}
|
||||
|
||||
private GoalJsonAcceptanceService.ConfigureRequest request(long revision, String... fields) {
|
||||
return new GoalJsonAcceptanceService.ConfigureRequest(revision, "report", List.of(fields));
|
||||
}
|
||||
|
||||
@Test void ownerCanPersistAndReviseRequirementsWithoutAcceptingAStaleEdit() {
|
||||
GoalEntity goal = goal(false);
|
||||
assertFalse(acceptance.get(goal.getId(), alice).required());
|
||||
var first = acceptance.configure(goal.getId(), "report-fields", request(0, "summary"), alice);
|
||||
assertEquals(1, first.revision());
|
||||
assertTrue(goals.toResponse(goals.getById(goal.getId())).isJsonAcceptanceRequired());
|
||||
assertEquals(List.of("summary"), acceptance.get(goal.getId(), alice).requirements().getFirst().requiredFields());
|
||||
var second = acceptance.configure(goal.getId(), "report-fields", request(1, "summary", "sources"), alice);
|
||||
assertEquals(2, second.revision());
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "report-fields", request(1, "forged"), alice));
|
||||
assertEquals(List.of("summary", "sources"), acceptance.get(goal.getId(), alice).requirements().getFirst().requiredFields());
|
||||
assertEquals(2, acceptance.configure(goal.getId(), "report-fields", request(2, "summary", "sources"), alice).revision());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void selectedContractBlocksBothSyntheticExplicitAndAutomaticCompletion(boolean automatic) {
|
||||
GoalEntity goal = goal(false);
|
||||
goals.appendCriterion(goal.getId(), "write report", alice);
|
||||
var forged = new GoalEvaluationResult(1, "report saved and checked", "completed", true, "fixture", 1, 0,
|
||||
List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "I verified the JSON")), null);
|
||||
goals.recordEvaluation(goal.getId(), forged, 1, 1);
|
||||
acceptance.configure(goal.getId(), "report-fields", request(0, "summary"), alice);
|
||||
assertThrows(MateClawException.class, () -> {
|
||||
if (automatic) goals.markEvaluatedCompleted(goal.getId(), forged);
|
||||
else goals.markCompleted(goal.getId(), forged);
|
||||
});
|
||||
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||
assertTrue(goals.listEvents(goal.getId(), 30).stream().noneMatch(e -> "completed".equals(e.getEventType())));
|
||||
}
|
||||
|
||||
@Test void unselectedLegacyCompletionRemainsCompatible() {
|
||||
GoalEntity goal = goal(false);
|
||||
assertEquals(GoalStatus.COMPLETED, goals.markCompleted(goal.getId(), null).getStatus());
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "late", request(0, "summary"), alice));
|
||||
}
|
||||
|
||||
@Test void unknownDisabledAndOtherUsersCannotConfigureOrReadEvenForSystemConversations() {
|
||||
GoalEntity goal = goal(false);
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "r", request(0, "summary"), null));
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "r", request(0, "summary"), bob));
|
||||
assertThrows(MateClawException.class, () -> acceptance.get(goal.getId(), bob));
|
||||
jdbc.update("UPDATE mate_user SET enabled=FALSE WHERE username=?", alice);
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "r", request(0, "summary"), alice));
|
||||
jdbc.update("UPDATE mate_conversation SET username='system' WHERE conversation_id=?", goal.getConversationId());
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "r", request(0, "summary"), "unknown-account"));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM mate_goal_json_requirement WHERE goal_id=?", Integer.class, goal.getId()));
|
||||
assertFalse(goals.getById(goal.getId()).isJsonAcceptanceRequired());
|
||||
}
|
||||
|
||||
@Test void rollbackDoesNotLeaveAContractOrEnableFlag() {
|
||||
GoalEntity goal = goal(false);
|
||||
new TransactionTemplate(transactions).executeWithoutResult(status -> {
|
||||
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||
status.setRollbackOnly();
|
||||
});
|
||||
assertFalse(acceptance.get(goal.getId(), alice).required());
|
||||
assertTrue(acceptance.get(goal.getId(), alice).requirements().isEmpty());
|
||||
}
|
||||
|
||||
@Test void competingUserEditsCannotBothCommitTheSameExpectedRevision() throws Exception {
|
||||
GoalEntity goal = goal(false);
|
||||
var start = new java.util.concurrent.CountDownLatch(1);
|
||||
try (var workers = java.util.concurrent.Executors.newFixedThreadPool(2)) {
|
||||
java.util.concurrent.Callable<Boolean> edit = () -> {
|
||||
start.await();
|
||||
try { acceptance.configure(goal.getId(), "r", request(0, "summary"), alice); return true; }
|
||||
catch (MateClawException conflict) {
|
||||
assertTrue(conflict.getMessage().contains("revision changed"));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var first = workers.submit(edit); var second = workers.submit(edit); start.countDown();
|
||||
assertNotEquals(first.get(10, java.util.concurrent.TimeUnit.SECONDS), second.get(10, java.util.concurrent.TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(1, acceptance.get(goal.getId(), alice).requirements().size());
|
||||
assertEquals(1, acceptance.get(goal.getId(), alice).requirements().getFirst().revision());
|
||||
}
|
||||
|
||||
@Test void malformedAndUnboundedContractsAreRejectedWithoutEnablingTheGoal() {
|
||||
GoalEntity goal = goal(false);
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "../r", request(0, "summary"), alice));
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "r", request(0, "summary", "summary"), alice));
|
||||
assertFalse(goals.getById(goal.getId()).isJsonAcceptanceRequired());
|
||||
for (int i=0; i<8; i++) acceptance.configure(goal.getId(), "r"+i, request(0, "summary"), alice);
|
||||
assertThrows(MateClawException.class, () -> acceptance.configure(goal.getId(), "overflow", request(0, "summary"), alice));
|
||||
assertEquals(8, acceptance.get(goal.getId(), alice).requirements().size());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user