test(goal): cover unselected queue goal race

This commit is contained in:
mateaix 2026-09-15 10:09:32 +08:00
parent ab2589550e
commit ad7b576c40
4 changed files with 36 additions and 6 deletions

View File

@ -53,7 +53,7 @@ The host clock, database credentials and service host remain trusted. Run arbitr
V198 also stores absolute scheduler lease deadlines. Existing leases expire during upgrade and are recovered from their persisted checkpoints: safe work may receive a new attempt; uncertain side effects remain blocked for review. Expired owners cannot renew, checkpoint, settle or use managed JSON tools. Renewal checks both current lease records after acquiring the goal lock, so a delayed scheduler tick cannot reuse an old timestamp to revive its owner. New valid owners can continue under the existing requirements. Recovery skips a scanned attempt while its continuation lease is still live or its owner has changed, so other eligible recoveries can proceed. A passing JSON binding does not override a pause caused by an uncertain tool outcome.
Validation snapshot (2026-09-15): the full default backend test run passed 5,359 executed tests with 46 conditional skips; the frontend passed 391 tests. The managed contract also has real compiled ReAct/Plan graph tests for account and scheduled-owner execution. Their model choices and semantic verdicts are controlled fixtures, not online-model benchmarks. Specialized integration profiles and the proprietary Kingbase engine are outside that full-default-suite claim.
Validation snapshot (2026-09-15): the full default backend test run passed 5,392 executed tests with 46 conditional skips; the frontend passed 391 tests. The managed contract also has real compiled ReAct/Plan graph tests for account and scheduled-owner execution. Its opt-in HTTP, approval and queue subset passed 32 tests each on MySQL 8.0.46 and PostgreSQL 16.14, including an explicitly unselected queued turn that becomes stale when a managed Goal appears; the broader managed JSON protocol suite passed 72 tests on each database. Their model choices and semantic verdicts are controlled fixtures, not online-model benchmarks. The MySQL run uses a test-only workaround for the unrelated V192 temporary-table migration, the PostgreSQL run uses the Kingbase-compatible migration tree with an unrelated skill bootstrap mocked, and the proprietary Kingbase engine remains outside these claims.
Built-in shell/code execution is not OS-isolated from the service host. Selecting JSON acceptance does not sandbox those tools, and the protocol cannot defend against host code that can access database credentials or files. Environment-name filtering and workspace path checks do not replace that isolation. Lease deadlines are calculated from absolute instants, including daylight-saving clock rollback; scheduling display fields remain local timestamps.

View File

@ -53,7 +53,7 @@ V197 使用 epoch 秒作为有效期依据,不受 JVM/JDBC 时区变化影响
V198 同样以绝对时间保存调度租约截止。升级时旧租约失效,按已有检查点恢复:安全工作可建立新 attempt不确定副作用仍阻断并要求核实。过期 owner 不能续租、提交检查点、结算或使用受管 JSON 工具。续租在获得 Goal 锁后检查两侧当前租约,迟到调度 tick 不能利用旧时间戳复活 owner新的有效 owner 可按现有要求继续。恢复扫描遇到 continuation 租约仍有效或 owner 已变化的记录时会跳过继续处理其他可恢复目标。JSON 绑定通过也不能越过不确定工具结果导致的暂停。
验证快照2026-09-15默认后端完整测试集实际通过 5,359 项、条件跳过 46 项,前端通过 391 项。受管契约还覆盖真实编译的 ReAct/Plan 图及账户/调度 owner 四种组合;模型选择和语义评价是受控夹具,不是在线模型基准。专门集成 profile 与 Kingbase 专有引擎不包含在“默认完整测试集”结论中。
验证快照2026-09-15默认后端完整测试集实际通过 5,392 项、条件跳过 46 项,前端通过 391 项。受管契约还覆盖真实编译的ReAct/Plan图及账户/调度owner组合MySQL 8.0.46与PostgreSQL 16.14各有32项显式HTTP、审批和队列子集通过包括明确未选定队列在等待期出现新受管Goal后判为过期各库更广的受管JSON协议集为72项通过。模型选择和语义评价是受控夹具,不是在线模型基准。MySQL使用仅供测试的无关V192临时表迁移替代PostgreSQL使用Kingbase兼容迁移树并mock无关skill启动Kingbase专有引擎仍不在这些结论中。
内置 shell/code 执行没有与服务宿主做操作系统隔离。选择 JSON 验收不会把这些工具变成沙箱;此协议不能抵抗能访问数据库凭据或文件的宿主代码,环境变量名称过滤和工作区路径检查也不能替代隔离。租约截止从绝对时刻计算,覆盖夏令时回拨;调度显示字段仍使用本地时间戳。

View File

@ -15,6 +15,7 @@ class GoalJsonExternalApprovalIntegrationTest extends GoalJsonHttpRuntimeIntegra
@Override
@ParameterizedTest
@CsvSource({"false,sync,true", "false,approval,true", "true,approval,true",
"false,queued-unselected-then-goal,true", "true,queued-unselected-then-goal,true",
"false,scheduled-queued,true", "true,scheduled-queued,true",
"false,scheduled-queued-foreign,true", "true,scheduled-queued-foreign,true",
"false,scheduled-queued-legacy,true", "true,scheduled-queued-legacy,true",

View File

@ -85,7 +85,8 @@ class GoalJsonHttpRuntimeIntegrationTest {
"false,scheduled-queued-terminal-unselected,true", "true,scheduled-queued-terminal-unselected,true",
"false,scheduled-queued-paused,true", "true,scheduled-queued-paused,true", "false,recovered,true", "true,recovered,true",
"false,scheduled,false", "true,scheduled,false", "false,recovered,false", "true,recovered,false",
"false,queued,true", "false,reuse,true", "true,reuse,true", "false,recheck,true", "true,recheck,true",
"false,queued,true", "false,queued-unselected-then-goal,true", "true,queued-unselected-then-goal,true",
"false,reuse,true", "true,reuse,true", "false,recheck,true", "true,recheck,true",
"false,supervised,true", "true,supervised,true", "false,supervised-recovered,true", "true,supervised-recovered,true",
"false,supervised,false", "true,supervised,false", "false,supervised-recovered,false", "true,supervised-recovered,false",
"false,approval,true", "true,approval,true", "false,scheduled-approval,true", "true,scheduled-approval,true",
@ -117,7 +118,8 @@ class GoalJsonHttpRuntimeIntegrationTest {
boolean scheduled = entry.startsWith("scheduled") || entry.equals("recovered") || supervised;
boolean reuse = entry.equals("reuse");
boolean recheck = entry.equals("recheck");
boolean queued = entry.equals("queued") || queuedPreflightRejected;
boolean queuedReplacement = entry.equals("queued-unselected-then-goal");
boolean queued = entry.equals("queued") || queuedReplacement || queuedPreflightRejected;
boolean recovered = entry.equals("recovered") || entry.equals("supervised-recovered");
String username = "http-json-" + UUID.randomUUID();
String conversation = UUID.randomUUID().toString();
@ -559,16 +561,43 @@ class GoalJsonHttpRuntimeIntegrationTest {
});
try {
assertTrue(firstSubscribed.await(10, java.util.concurrent.TimeUnit.SECONDS), "Initial HTTP turn must reach the actual model boundary");
if (queuedReplacement) goals.abandon(goal.getId(), username);
JsonNode enqueue = request("POST", "/api/v1/chat/" + conversation + "/interrupt", token,
Map.of("agentId", String.valueOf(agentId), "message", message));
assertTrue(enqueue.path("data").path("queued").asBoolean(), enqueue.toString());
long queueId = Long.parseLong(enqueue.path("data").path("queueItemId").asText());
assertEquals(userId, jdbc.queryForObject("SELECT requester_user_id FROM mate_conversation_input_queue WHERE id=?", Long.class, queueId));
GoalEntity replacement = null;
if (queuedReplacement) {
assertEquals(0L, jdbc.queryForObject(
"SELECT selected_goal_id FROM mate_conversation_input_queue WHERE id=?", Long.class, queueId));
var replacementCreate = new GoalCreateRequest(); replacementCreate.setConversationId(conversation);
replacementCreate.setAgentId(agentId); replacementCreate.setWorkspaceId(1L);
replacementCreate.setTitle("Managed Goal created while input waits");
replacementCreate.setDescription("Do not attach the explicit zero queue snapshot");
replacement = goals.create(replacementCreate, username);
JsonNode replacementConfigured = request("PUT", "/api/v1/goals/" + replacement.getId()
+ "/json-acceptance/requirements/r", token,
Map.of("expectedRevision", "0", "artifactSlot", "report", "requiredFields", List.of("summary")));
assertEquals(200, replacementConfigured.path("code").asInt(), replacementConfigured.toString());
}
String initialAnswer = plan
? "{\"needs_planning\":false,\"direct_answer\":\"Initial fixture turn finished.\"}"
: "Initial fixture turn finished.";
assertEquals(reactor.core.publisher.Sinks.EmitResult.OK, initialResponse.tryEmitValue(
new ChatResponse(List.of(new Generation(new AssistantMessage("Initial fixture turn finished."))))));
new ChatResponse(List.of(new Generation(new AssistantMessage(initialAnswer))))));
String events = response.get(30, java.util.concurrent.TimeUnit.SECONDS);
assertTrue(events.contains("Managed JSON fixture completed."), events);
assertEquals("consumed", jdbc.queryForObject("SELECT state FROM mate_conversation_input_queue WHERE id=?", String.class, queueId));
if (queuedReplacement) {
assertTrue(events.contains("queued_input_skipped"), events);
assertEquals(0, calls.get(), "The explicit zero snapshot must not start a second model turn");
assertEquals(GoalStatus.ACTIVE, goals.getById(replacement.getId()).getStatus());
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM mate_goal_json_artifact WHERE goal_id=?",
Integer.class, replacement.getId()));
return;
} else {
assertTrue(events.contains("Managed JSON fixture completed."), events);
}
} finally {
initialResponse.tryEmitEmpty();
response.cancel(true);