Skip to content

Commit fd4af70

Browse files
wenshaoqwen-code-ci-botqwencoder
authored
test(managed-agent): pin untested contracts from the #12692 R2 review (#13348)
* test(managed-agent): pin untested contracts from the #12692 R2 review Twelve R2 review follow-ups on #12692 in the test surface, re-verified against current main (2c591ec) before writing each witness: - SessionEventHub evicts beyond CAPACITY (512) and reports whether the watermark still held the requested position, pinning the overflow signal the SSE fold relies on. - HarnessCoordinator fails the turn when a recovered runtime is not ready for continuation, fails terminally when the recovery moved between the witness read and the bind, and schedules a retry when the admission epoch moved before commit. - HarnessEventProjector substitutes the safe fallback code for an out-of-alphabet error code, and the secret-redaction sweep now holds both the key and the value of a sensitive attribute out of the leaked map. - QwenHostedHarnessConnector's unknown-outcome create falls back to load and propagates the full attachment (boot id, recovery handle, generate and event epoch); the load path propagates the same four fields; a non-conflict daemon error is rethrown without any fallback load. - ManagedSessionStore contract fixtures assert per-record session identity, record count by the records list, and a monotone sequence range with an event count that fits. - ManagedSessionStore integration holds the per-page restore byte budget: twelve dense two-line commits with a 1 MiB pad page flush the first page after nine transactions (revisions 1..9, hasMore=true) and finish on the second page (revisions 10..12, hasMore=false). - The MariaDB MySqlIT is rerunnable against a persistent database: the legacy fixture namespaces every fixed identity per run (upgrade/lifecycle/hooks tenants and sessions), skips the migration-bound assertions (V15/V17/V29 were applied once and cannot re-apply to a rerun's seeds), and re-seeds the V9 scope conflict source explicitly so the workspace idempotency probe is not migration-order dependent. The projection, private-store and lease-precision sub-exercises are equally namespaced so a second mvn failsafe run on the same database is green. - ManagedAgentServerIntegrationTest fixture harness is keyed by tenant for the runtime-recovery attachment, and the dispatchable turns it claims are cleaned up between sub-cases so the retries, the generation transfer and the epoch retraction stay deterministic against a shared H2. Verified on the managed-agent-server module with mvn verify after same-source reinstall of qwencode and runtime-broker (alpha-jar md5 fingerprints matched before and after): witness runs of the ManagedAgentMySqlIT against a persistent temp MariaDB passed twice in a row (19 tests, 0 failures, 0 errors each). * fix(managed-agent): close Java test witness gaps from review (#13348) - ManagedAgentMySqlIT: derive the first-pass gate from this run's own Flyway target-15 migrate result instead of V15's item_id output, so a V15 regression can no longer silence its own witnesses; correct the rerun comment (consumer_progress is gated, agent_revision reads V13's DEFAULT) and merge the adjacent gate blocks. - ManagedAgentMySqlIT: derive the journal case-sensitivity probe from the namespaced storeTenant so it stays a pure case variant. - ManagedAgentMySqlIT: drop the inert per-connection SET FOREIGN_KEY_CHECKS toggle; the child-first delete order is the real invariant and is now stated as such. - HarnessEventProjectorTest: assert secret absence on the serialized data (any nesting depth), pin the raw code's absence from the fallback-code event, and add the oversize/at-bound error-code arms for SAFE_ERROR_CODE's {1,128} bound. - ManagedSessionStoreContractFixtureTest: assert the transaction summary rule the server actually enforces, branching on operation (genesis zero rule vs range/record equality). * test(managed-agent): settle Turn residue in one sweep and pin the watermark operand (#13348) - ManagedAgentServerIntegrationTest: replace the per-call-site cleanupTurn helper with one @AfterEach sweep that settles every non-terminal Turn before the recovery scanner re-arms, covering the two retraction tests that never invoked the helper; the concurrent-submit finally whose re-await could mask the real failure goes away with it. - ManagedAgentMySqlIT: extend the warm-database delete list with managed_agent_operation and managed_workspace_create_command so it covers all seven RESTRICT children of managed_agent_session. - ManagedSessionStoreContractFixtureTest: drop the unreachable non-genesis else arm; pin the published genesis shape unconditionally. - QwenHostedHarnessConnectorTest: route the three new recovery tests through the existing connector(client) helper. - HarnessCoordinatorTest: add retriesTheTurnWhenTheRecoveredWatermarkRegresses — same epoch, lower admission watermark — the unique witness for the lastEventId guard disjunct (mutation probe: deleting the disjunct fails exactly this test). Co-authored-by: Qwen-Coder <[email protected]> * test(managed-agent): pin single-drive guards and namespace case tenants (#13348) Co-authored-by: Qwen-Coder <[email protected]> * chore(managed-agent): re-verify round-4 test fixes after lint-gate rejection (#13348) The deterministic gate rejected 3a3b203 on npm run lint, but the 589 errors were all in untracked, git-ignored Maven javadoc output under packages/sdk-java/qwencode/target/reports/apidocs/ left in the runner workspace by a same-round mvn package/install of the qwencode module - not in anything the commit touched. The committed tree needs no change; this empty follow-up records the repair: the generated output was removed from the workspace and npm run build, npm run typecheck, and npm run lint all pass, with mvn -Dtest=HarnessCoordinatorTest test (33/33) and mvn checkstyle:check green. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> * test(managed-agent): gate upgrade witnesses on their own migration versions (#13348) - ManagedAgentMySqlIT: replace the single firstPass flag with BackfillWitnesses derived from the versions Flyway had applied at the run's start. One shared flag reads a database left mid-window as a first pass (V2's consumer_progress count then fails on rows V2 never saw) or as a rerun (the V17 and V29 witnesses then silently skip over freshly seeded rows their migrations did migrate). The V9 re-seed now keys on V9's application too: at V14 firstPass is true, so the old keying skipped the re-seed and the idempotency_conflict probe failed. - BackfillWitnessesTest: pin the gate tuple against H2 databases staged at every boundary version (fresh, 1, 8, 9, 14, 15, 16, 17, 28, 29, head); reverting to the shared flag reds it. - QwenHostedHarnessConnectorTest: stub getSuppressed()/getStackTrace() on the mocked unknown-outcome exception so a regressed fallback reports the failing test's name instead of aborting the class report on a null array; capture the LoadHarnessSession and pin the passiveManagedRuntimeRecovery=false operand. - SessionEventHubTest: await one below the watermark so CAPACITY is pinned from above as well (a 513 ring now reds the witness). - HarnessEventProjectorTest: witness the tool_call allowlist, the sibling of the text-path redaction the PR already pinned (the raw update leak mutant now dies). Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: qwen-code-ci-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]>
1 parent 28e0f3a commit fd4af70

10 files changed

Lines changed: 759 additions & 52 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.alibaba.qwen.code.managedagent;
2+
3+
import java.util.HashSet;
4+
import java.util.Set;
5+
import javax.sql.DataSource;
6+
import org.flywaydb.core.Flyway;
7+
import org.flywaydb.core.api.MigrationInfo;
8+
9+
/**
10+
* The one-shot backfills behind the upgrade witnesses of
11+
* {@link ManagedAgentMySqlIT}: each applies only to the rows present when
12+
* it runs, so its witness is valid only when its migration was still
13+
* pending at the run's start. Gating all of them on one shared flag reads
14+
* a database left mid-window by an interrupted or older run as a first
15+
* pass (the V2-derived count then fails) or as a rerun (the V17 and V29
16+
* witnesses then silently skip).
17+
*/
18+
record BackfillWitnesses(boolean consumerProgress, boolean eventIdentity,
19+
boolean pendingOperations, boolean hookAdmissions,
20+
boolean reseedCreationScope) {
21+
static BackfillWitnesses forApplied(Set<String> appliedAtStart) {
22+
return new BackfillWitnesses(!appliedAtStart.contains("2"),
23+
!appliedAtStart.contains("15"), !appliedAtStart.contains("17"),
24+
!appliedAtStart.contains("29"), appliedAtStart.contains("9"));
25+
}
26+
27+
/** Versions Flyway had applied before this run migrates anything. */
28+
static Set<String> appliedVersions(DataSource dataSource) {
29+
Set<String> versions = new HashSet<>();
30+
for (MigrationInfo info : Flyway.configure().dataSource(dataSource)
31+
.locations("classpath:db/migration").load().info()
32+
.applied()) {
33+
if (info.getVersion() != null) {
34+
versions.add(info.getVersion().toString());
35+
}
36+
}
37+
return versions;
38+
}
39+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.alibaba.qwen.code.managedagent;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.UUID;
6+
import org.flywaydb.core.Flyway;
7+
import org.flywaydb.core.api.MigrationVersion;
8+
import org.flywaydb.core.api.configuration.FluentConfiguration;
9+
import org.h2.jdbcx.JdbcDataSource;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* The witness gates of {@link ManagedAgentMySqlIT} against databases staged
14+
* at each boundary version: a witness opens exactly when its one-shot
15+
* backfill was still pending at the run's start, so a database left
16+
* mid-window (V14 keeps the V15, V17 and V29 witnesses open while closing
17+
* V2's, V16 closes V15's) neither fails the V2-derived count nor silently
18+
* skips the later witnesses.
19+
*/
20+
class BackfillWitnessesTest {
21+
@Test
22+
void gatesFollowTheVersionsPendingAtRunStart() {
23+
// staged -> (consumerProgress V2, eventIdentity V15,
24+
// pendingOperations V17, hookAdmissions V29, reseedCreationScope V9)
25+
assertGates(null, true, true, true, true, false);
26+
assertGates("1", true, true, true, true, false);
27+
assertGates("8", false, true, true, true, false);
28+
assertGates("9", false, true, true, true, true);
29+
assertGates("14", false, true, true, true, true);
30+
assertGates("15", false, false, true, true, true);
31+
assertGates("16", false, false, true, true, true);
32+
assertGates("17", false, false, false, true, true);
33+
assertGates("28", false, false, false, true, true);
34+
assertGates("29", false, false, false, false, true);
35+
assertGates("head", false, false, false, false, true);
36+
}
37+
38+
private static void assertGates(String staged, boolean consumerProgress,
39+
boolean eventIdentity, boolean pendingOperations,
40+
boolean hookAdmissions, boolean reseedCreationScope) {
41+
JdbcDataSource dataSource = new JdbcDataSource();
42+
dataSource.setURL("jdbc:h2:mem:witness-gates-" + UUID.randomUUID()
43+
+ ";MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE");
44+
if (staged != null) {
45+
FluentConfiguration flyway = Flyway.configure()
46+
.dataSource(dataSource)
47+
.locations("classpath:db/migration");
48+
if (!"head".equals(staged)) {
49+
flyway.target(MigrationVersion.fromVersion(staged));
50+
}
51+
flyway.load().migrate();
52+
}
53+
assertThat(BackfillWitnesses.forApplied(
54+
BackfillWitnesses.appliedVersions(dataSource)))
55+
.as("database staged at %s", staged)
56+
.isEqualTo(new BackfillWitnesses(consumerProgress,
57+
eventIdentity, pendingOperations, hookAdmissions,
58+
reseedCreationScope));
59+
}
60+
}

‎packages/sdk-java/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/HarnessEventProjectorTest.java‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ void projectsTextWithoutLeakingOtherUpdateFields() {
2626
.containsEntry("itemId", "item_turn-1_assistant")
2727
.containsEntry("contentPartId",
2828
"part_turn-1_output_text");
29+
// containsEntry cannot prove absence: the raw update map must never
30+
// bleed through into the projected, persisted and re-streamed data.
31+
// The secret is nested two levels down, so absence is asserted on
32+
// the serialized form, which holds at any nesting depth.
33+
assertThat(event.data().toString()).doesNotContain("secret")
34+
.doesNotContain("must-not-leak");
2935
}
3036

3137
@Test
@@ -51,6 +57,57 @@ void redactsHarnessErrorDetails() {
5157
.doesNotContain("/private/workspace");
5258
}
5359

60+
@Test
61+
void projectsToolCallsWithoutLeakingOtherUpdateFields() {
62+
ProjectedEvent event = projector.project(new SourceEvent(7L,
63+
"session_update", Map.of("update", Map.of(
64+
"sessionUpdate", "tool_call",
65+
"toolCallId", "tool-1",
66+
"name", "read_file",
67+
"title", "Read file",
68+
"status", "completed",
69+
"rawInput", Map.of("path", "/private/workspace"),
70+
"secret", "must-not-leak")), "prompt", Map.of()),
71+
"turn-1");
72+
73+
assertThat(event.type()).isEqualTo("item.tool_call.updated");
74+
assertThat(event.data()).containsEntry("toolCallId", "tool-1")
75+
.containsEntry("name", "read_file")
76+
.containsEntry("title", "Read file")
77+
.containsEntry("status", "completed");
78+
// The same allowlist guards the tool path: the raw update map must
79+
// never bleed through, at any nesting depth.
80+
assertThat(event.data().toString()).doesNotContain("secret")
81+
.doesNotContain("must-not-leak")
82+
.doesNotContain("/private/workspace")
83+
.doesNotContain("rawInput");
84+
}
85+
86+
@Test
87+
void substitutesTheSafeFallbackCodeForAnOutOfAlphabetErrorCode() {
88+
ProjectedEvent event = projector.project(new SourceEvent(4L,
89+
"turn_error", Map.of("code", "sql=1; DROP TABLE users --",
90+
"message", "tagged"), "prompt", Map.of()), "turn-1");
91+
92+
assertThat(event.errorCode()).isEqualTo("hosted_harness_error");
93+
assertThat(event.type()).isEqualTo("turn.failed");
94+
assertThat(event.data().toString()).doesNotContain("DROP TABLE");
95+
}
96+
97+
@Test
98+
void substitutesTheSafeFallbackCodeForAnOversizeErrorCode() {
99+
ProjectedEvent oversize = projector.project(new SourceEvent(5L,
100+
"turn_error", Map.of("code", "a".repeat(129), "message",
101+
"tagged"), "prompt", Map.of()), "turn-1");
102+
ProjectedEvent atBound = projector.project(new SourceEvent(6L,
103+
"turn_error", Map.of("code", "b".repeat(128), "message",
104+
"tagged"), "prompt", Map.of()), "turn-2");
105+
106+
assertThat(oversize.errorCode()).isEqualTo("hosted_harness_error");
107+
assertThat(oversize.type()).isEqualTo("turn.failed");
108+
assertThat(atBound.errorCode()).isEqualTo("b".repeat(128));
109+
}
110+
54111
@Test
55112
void projectsDeadlineExpiryAsAClassifiedFailure() {
56113
ProjectedEvent event = projector.project(new SourceEvent(4L,

‎packages/sdk-java/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/ManagedAgentMySqlIT.java‎

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -71,55 +71,91 @@ class ManagedAgentMySqlIT {
7171
@Order(1)
7272
void upgradesAndExercisesStoresOnMySql() throws IOException {
7373
DriverManagerDataSource dataSource = dataSource();
74+
BackfillWitnesses witnesses = BackfillWitnesses.forApplied(
75+
BackfillWitnesses.appliedVersions(dataSource));
7476
Flyway.configure().dataSource(dataSource)
7577
.locations("classpath:db/migration")
7678
.target(MigrationVersion.fromVersion("1")).load().migrate();
7779
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
80+
// A rerun sees every prior run's rows. The legacy seeds are
81+
// namespaced per run so the second pass of this method does not
82+
// collide with the first.
83+
String runId = UUID.randomUUID().toString().substring(0, 8);
84+
String upgradeTenant = "mysql-upgrade-" + runId;
85+
String upgradeSession = "session_upgrade_" + runId;
86+
String lifecycleTenant = "mysql-lifecycle-" + runId;
87+
String hooksTenant = "mysql-hooks-" + runId;
88+
String hooksSession = "session_hooks_" + runId;
7889
jdbc.update("INSERT INTO managed_agent_session (tenant_id,"
7990
+ " session_id, agent_id, status, created_at,"
8091
+ " updated_at) VALUES (?, ?, ?, ?, ?, ?)",
81-
"mysql-upgrade", "session_upgrade", "qwen-code", "IDLE",
92+
upgradeTenant, upgradeSession, "qwen-code", "IDLE",
8293
1L, 1L);
8394
jdbc.update("INSERT INTO managed_agent_command (tenant_id, operation,"
8495
+ " idempotency_key, request_digest, session_id, created_at)"
85-
+ " VALUES ('mysql-upgrade', 'CREATE_SESSION', 'legacy-key',"
86-
+ " 'legacy-digest', 'session_upgrade', 1)");
87-
LegacyEvents.insert(jdbc, "mysql-upgrade", "session_upgrade");
96+
+ " VALUES (?, 'CREATE_SESSION', 'legacy-key',"
97+
+ " 'legacy-digest', ?, 1)", upgradeTenant, upgradeSession);
98+
LegacyEvents.insert(jdbc, upgradeTenant, upgradeSession);
8899
Flyway.configure().dataSource(dataSource)
89100
.locations("classpath:db/migration")
90101
.target(MigrationVersion.fromVersion("15")).load().migrate();
91102
LegacyLifecycleCommands.Sessions lifecycle =
92-
LegacyLifecycleCommands.insert(jdbc, "mysql-lifecycle");
103+
LegacyLifecycleCommands.insert(jdbc, lifecycleTenant);
93104
Flyway.configure().dataSource(dataSource)
94105
.locations("classpath:db/migration")
95106
.target(MigrationVersion.fromVersion("27")).load().migrate();
96-
LegacyHookRecords.insert(jdbc, "mysql-hooks", "session_hooks");
107+
LegacyHookRecords.insert(jdbc, hooksTenant, hooksSession);
97108
Flyway.configure().dataSource(dataSource)
98109
.locations("classpath:db/migration").load().migrate();
99-
LegacyEvents.assertBackfilled(jdbc, "mysql-upgrade",
100-
"session_upgrade");
101-
LegacyLifecycleCommands.assertMigrated(jdbc, "mysql-lifecycle",
102-
lifecycle);
103-
LegacyHookRecords.assertBackfilled(jdbc, "mysql-hooks", "session_hooks");
104-
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM"
105-
+ " managed_agent_consumer_progress WHERE tenant_id = ?"
106-
+ " AND session_id = ? AND consumer_name = ?",
107-
Integer.class, "mysql-upgrade", "session_upgrade",
108-
"message_projection")).isEqualTo(1);
110+
// The gates come from what this run found already applied, never
111+
// from the migrated rows: gating on V15's item_id backfill would
112+
// let a V15 regression silently switch off its own witnesses. A
113+
// rerun still exercises the store through the idempotency conflict
114+
// re-seeded below and the fresh-projection exercises;
115+
// agent_revision there reads V13's column DEFAULT.
116+
if (witnesses.eventIdentity()) {
117+
LegacyEvents.assertBackfilled(jdbc, upgradeTenant, upgradeSession);
118+
}
119+
if (witnesses.pendingOperations()) {
120+
LegacyLifecycleCommands.assertMigrated(jdbc, lifecycleTenant,
121+
lifecycle);
122+
}
123+
if (witnesses.hookAdmissions()) {
124+
LegacyHookRecords.assertBackfilled(jdbc, hooksTenant,
125+
hooksSession);
126+
}
127+
if (witnesses.consumerProgress()) {
128+
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM"
129+
+ " managed_agent_consumer_progress WHERE tenant_id = ?"
130+
+ " AND session_id = ? AND consumer_name = ?",
131+
Integer.class, upgradeTenant, upgradeSession,
132+
"message_projection")).isEqualTo(1);
133+
}
109134
assertThat(jdbc.queryForObject("SELECT agent_revision FROM"
110135
+ " managed_agent_session WHERE session_id = ?",
111-
String.class, "session_upgrade")).isEqualTo("1");
136+
String.class, upgradeSession)).isEqualTo("1");
112137
ManagedAgentStore store = new ManagedAgentStore(
113138
jdbc, new ObjectMapper(), Clock.systemUTC(), ignored -> {
114139
}, new com.alibaba.qwen.code.managedagent.store.ManagedWorkspaceRegistry(jdbc),
115140
new ManagedAgentProperties());
141+
// The V9 scope backfill marked the legacy creation key
142+
// workspace-unbound when it ran; when V9 was already applied this
143+
// run's seed missed it, so the conflict source is re-seeded
144+
// explicitly and the probe does not depend on the migration order.
145+
if (witnesses.reseedCreationScope()) {
146+
jdbc.update("INSERT INTO managed_session_create_scope (tenant_id,"
147+
+ " idempotency_key, workspace_bound) VALUES (?, ?,"
148+
+ " FALSE) ON DUPLICATE KEY UPDATE workspace_bound"
149+
+ " = workspace_bound", upgradeTenant,
150+
"legacy-key");
151+
}
116152
assertThatThrownBy(() -> store.insertWorkspaceSessionCommand(
117-
"mysql-upgrade", "actor", "legacy-key", "bound-digest",
153+
upgradeTenant, "actor", "legacy-key", "bound-digest",
118154
"qwen-code", null, null, List.of(), null,
119155
new com.alibaba.qwen.code.managedagent.api.WorkspaceSelection("ws-a", ".")))
120156
.isInstanceOfSatisfying(ApiException.class, error ->
121157
assertThat(error.getCode()).isEqualTo("idempotency_conflict"));
122-
String tenant = "mysql-projection";
158+
String tenant = "mysql-projection-" + runId;
123159
List<Map<String, Object>> input = List.of(Map.of(
124160
"type", "text", "text", "hello"));
125161
Admission admission = store.insertSessionCommand(tenant,
@@ -181,9 +217,9 @@ jdbc, new ObjectMapper(), Clock.systemUTC(), ignored -> {
181217
new DataSourceTransactionManager(dataSource));
182218
ManagedSessionStore firstInstance = new ManagedSessionStore(jdbc);
183219
ManagedSessionStore secondInstance = new ManagedSessionStore(jdbc);
184-
String storeTenant = "mysql-private-store";
185-
String sessionId = "mysql-private-session";
186-
String workspaceId = "mysql-private-workspace";
220+
String storeTenant = "mysql-private-store-" + runId;
221+
String sessionId = "mysql-private-session-" + runId;
222+
String workspaceId = "mysql-private-workspace-" + runId;
187223
String tokenA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
188224
String tokenB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
189225
WriterGrant firstGrant = inTransaction(transactions,
@@ -232,7 +268,8 @@ resourceBytes.length, sha256("mysql-resource"),
232268
sessionId, "mysql-resource", tokenA)).bytes())
233269
.isEqualTo(resourceBytes);
234270
assertThatThrownBy(() -> inTransaction(transactions,
235-
() -> secondInstance.restore("MYSQL-PRIVATE-STORE",
271+
() -> secondInstance.restore(
272+
storeTenant.toUpperCase(java.util.Locale.ROOT),
236273
workspaceId, sessionId, tokenA)))
237274
.isInstanceOfSatisfying(ApiException.class, error ->
238275
assertThat(error.getCode()).isEqualTo(
@@ -326,13 +363,9 @@ void shortWriterLeaseKeepsSubsecondDatabasePrecision(String connectionTimeZone)
326363
assertThat(now).isNotNull();
327364
assertThat(grant.leaseUntil() - now)
328365
.isBetween(700L, 1_000L);
329-
Long persistedLeaseMicros = jdbc.queryForObject(
330-
"SELECT TIMESTAMPDIFF(MICROSECOND, CURRENT_TIMESTAMP(6),"
331-
+ " writer_lease_until) FROM"
332-
+ " qwen_managed_session_journal_head WHERE tenant_id"
333-
+ " = ? AND session_id = ?",
334-
Long.class, tenant, session);
335-
assertThat(persistedLeaseMicros).isPositive();
366+
// The sub-second precision is pinned by assertLeaseDeadline's exact
367+
// stored-vs-app deadline equality; a bare isPositive cannot detect
368+
// fraction truncation.
336369
assertLeaseDeadline(jdbc, tenant, session, grant);
337370

338371
WriterGrant reacquired = inTransaction(transactions,
@@ -425,25 +458,31 @@ void isolatesTenantsThatDifferOnlyByCase() {
425458
DriverManagerDataSource dataSource = dataSource();
426459
Flyway.configure().dataSource(dataSource)
427460
.locations("classpath:db/migration").load().migrate();
461+
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
462+
// Per-run tenant ids keep reruns on a shared database isolated with
463+
// no cleanup; the pair differs only by case, the property under
464+
// test (tenant_id is utf8mb4_bin).
465+
String lowerTenant = "case-" + UUID.randomUUID().toString()
466+
.substring(0, 8);
467+
String upperTenant = lowerTenant.toUpperCase(java.util.Locale.ROOT);
428468
ManagedAgentStore store = new ManagedAgentStore(
429-
new JdbcTemplate(dataSource), new ObjectMapper(),
430-
Clock.systemUTC(), ignored -> {
469+
jdbc, new ObjectMapper(), Clock.systemUTC(), ignored -> {
431470
}, new com.alibaba.qwen.code.managedagent.store.ManagedWorkspaceRegistry(
432-
new JdbcTemplate(dataSource)),
471+
jdbc),
433472
new ManagedAgentProperties());
434-
Admission lower = store.insertSessionCommand("case-tenant",
473+
Admission lower = store.insertSessionCommand(lowerTenant,
435474
"CREATE_SESSION", "case-key", "case-digest", "qwen-code", null,
436475
null, List.of(), null);
437-
Admission upper = store.insertSessionCommand("CASE-TENANT",
476+
Admission upper = store.insertSessionCommand(upperTenant,
438477
"CREATE_SESSION", "case-key", "case-digest", "qwen-code", null,
439478
null, List.of(), null);
440479

441480
assertThat(upper.sessionId()).isNotEqualTo(lower.sessionId());
442-
assertThat(store.findSession("CASE-TENANT", lower.sessionId()))
481+
assertThat(store.findSession(upperTenant, lower.sessionId()))
443482
.isEmpty();
444-
assertThat(store.findSession("case-tenant", upper.sessionId()))
483+
assertThat(store.findSession(lowerTenant, upper.sessionId()))
445484
.isEmpty();
446-
assertThat(store.listSessions("CASE-TENANT", null, null, null, 10)
485+
assertThat(store.listSessions(upperTenant, null, null, null, 10)
447486
.sessions()).extracting(session -> session.sessionId())
448487
.containsExactly(upper.sessionId());
449488
}

0 commit comments

Comments
 (0)