test(managed): align recovery and transcript fixture contracts (#15899)

* test(managed): record which load path produced each post-crash entry

HostedProcessCrashIT fails on main with postFailureLoads.every(Boolean), but
the recorded sequence changes shape between runs (#15898: 9 vs 10 entries,
different true positions, one run with none), so the current message cannot
say which of the connector's load paths sent the offending request.

Record a compact origin discriminator per entry — cancellationTakeover,
passiveManagedRuntimeRecovery, driveRuntimeRecovery, mcpManagementOnly and
whether cancellationPromptId was present — and include it in both assertion
messages. The assertion itself is unchanged: 'no tool replay after a crash'
is the property it protects, and the next red run should name its own origin
instead of being guessed at.

* test(managed): name the load path behind each post-crash entry

Both review findings were correct: the origin string added in fad5d81a
still could not name the culprit it exists to name.

Two load shapes reach POST /session/:id/load carrying markers the origin
string never read, so they rendered byte-identical to a bare re-attach:

- a recovery-cleanup load sets only recoveryCleanupPromptId
  (LoadHarnessSession.forRecoveryCleanup throws when passive/drive/
  takeover/lifecycle/promptId are set at all), and
  QwenHostedHarnessConnector.doRecheckRecoveryCleanup issues one;
- a lifecycle load sets only lifecycleAuthority
  (QwenHostedHarnessConnector.lifecycleAttachment builds it with
  passive=drive=takeover=false).

promptId could not rescue them: withCancellationPromptId throws unless
passiveManagedRuntimeRecovery && cancellationTakeover, so promptId=true
implies takeover=true and the marker is always false on exactly the
entries every(Boolean) rejects.

Recording also happened eleven lines before await fetch(target, ...), so
a load the daemon refused was indistinguishable from an admitted one.
This driver already expects that refusal for a plain attach
(assert.equal(loaded.error, 'hosted_turn_recovery_required'), HTTP 409),
and the connector then performs the takeover load that actually replaces
the runtime. Move the recording after the response and add status, so a
refused probe that changed nothing no longer prints as the forbidden
admitted plain re-attach.

Verified by mutation over the eight load shapes that can reach this
endpoint: the previous marker set collapsed four of them (refused bare
reattach, admitted bare reattach, recovery-cleanup, lifecycle) into one
indistinguishable string; each of the three added markers is
load-bearing, since dropping any one reintroduces a collision. The
assertion itself is unchanged.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvugxtx22a

* test(managed): distinguish cleanup loads and response outcomes

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed): align recovery and transcript fixture contracts

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
易良andQwen-Coder authored and GitHub committed 2026-10-30 18:17:02 +08:00
1 parent 0976826090
commit d896c1816a
2 files changed
+58 -10

No files matched your search

@@ -50,6 +50,10 @@ const report = {
promptSubmissions: 0,
continuationsAfterFailure: 0,
postFailureLoads: [] as boolean[],
postFailureCancellationLoads: 0,
// Record non-resuming recovery modes separately from their wire origins.
// Both modes target the original prompt; cleanup alone cannot prove takeover.
postFailureLoadOrigins: [] as string[],
lateCancelReplays: 0,
originalCancelBody: '',
};
@@ -156,8 +160,44 @@ const proxy = createServer(async (req, res) => {
`/session/${config.sessionId}/managed-runtime/continue`
)
report.continuationsAfterFailure++;
if (injected && target.pathname.endsWith('/load'))
report.postFailureLoads.push(fields.cancellationTakeover === true);
let postFailureLoadIndex: number | undefined;
if (injected && target.pathname.endsWith('/load')) {
const cancellationOnly =
fields.cancellationTakeover === true &&
fields.passiveManagedRuntimeRecovery === true &&
typeof fields.cancellationPromptId === 'string' &&
fields.cancellationPromptId === report.promptId &&
fields.recoveryCleanupPromptId === undefined;
const cleanupOnly =
typeof fields.recoveryCleanupPromptId === 'string' &&
fields.recoveryCleanupPromptId === report.promptId &&
fields.cancellationTakeover !== true &&
fields.passiveManagedRuntimeRecovery !== true &&
fields.cancellationPromptId === undefined;
const nonDriving =
fields.driveRuntimeRecovery !== true &&
fields.mcpManagementOnly !== true &&
fields.lifecycleAuthority === undefined;
postFailureLoadIndex = report.postFailureLoads.length;
report.postFailureLoads.push(
nonDriving && (cancellationOnly || cleanupOnly),
);
if (nonDriving && cancellationOnly)
report.postFailureCancellationLoads++;
report.postFailureLoadOrigins.push(
[
`takeover=${fields.cancellationTakeover === true}`,
`passive=${fields.passiveManagedRuntimeRecovery === true}`,
`drive=${fields.driveRuntimeRecovery === true}`,
`mcpOnly=${fields.mcpManagementOnly === true}`,
`promptId=${typeof fields.cancellationPromptId === 'string'}`,
`promptMatches=${fields.cancellationPromptId === report.promptId}`,
`cleanup=${typeof fields.recoveryCleanupPromptId === 'string'}`,
`cleanupMatches=${fields.recoveryCleanupPromptId === report.promptId}`,
`lifecycle=${fields.lifecycleAuthority !== undefined}`,
].join(' '),
);
}
const headers = Object.fromEntries(
Object.entries(req.headers)
.filter(
@@ -181,6 +221,10 @@ const proxy = createServer(async (req, res) => {
headers,
...(body.length ? { body } : {}),
});
if (postFailureLoadIndex !== undefined) {
report.postFailureLoadOrigins[postFailureLoadIndex] +=
` status=${upstream.status}`;
}
if (target.pathname.endsWith('/events') && upstream.body) {
res.writeHead(upstream.status, relayedHeaders(upstream.headers));
const reader = upstream.body.getReader();
@@ -679,10 +723,17 @@ async function runRecoverySettlement() {
assert.equal(report.modelCalls, 1);
assert.equal(report.promptSubmissions, 1);
assert.equal(report.continuationsAfterFailure, 0);
assert(report.postFailureLoads.length > 0);
assert(
report.postFailureLoads.length > 0,
`no post-failure loads were recorded at all: ${JSON.stringify(report.postFailureLoadOrigins)}`,
);
assert(
report.postFailureCancellationLoads > 0,
'replacement must use a targeted cancellation takeover load',
);
assert(
report.postFailureLoads.every(Boolean),
'replacement must use cancellation-only loads',
`replacement must use targeted cancellation or cleanup-only loads: ${report.postFailureLoadOrigins.join(' | ')}`,
);
await unlink(`${proof}.read-gate`);
await waitUntil(async () => {
@@ -304,15 +304,12 @@ try {
.join(''),
'FG6e tool finished once.',
);
// Progress frames materialize as transcript items, not transcript events.
const itemFrames = new Set([
'item.output_text.delta',
'item.tool_call.updated',
]);
// Message pages carry selected Turn control events; full Session history
// and progress frames are checked against the SQL ledger above.
assert.deepEqual(
transcript.events,
webFrames
.filter((frame) => !itemFrames.has(frame.event))
.filter((frame) => frame.event === 'turn.completed')
.map((frame) => frame.data),
);
assert.equal(