fix(coding-agent): gate print-mode plan arming on settings before session context

- runPrintMode dereferenced session.sessionManager before checking whether
  plan.defaultOnStartup was even enabled, crashing partial-session harnesses;
  the settings check now short-circuits first.
- Extended the print-mode session fakes with the settings/sessionManager
  members every real session provides.
- Scoped the rpc-stdin-lock stderr assertion to non-load-notice lines: the
  adversarial fixture's load failure is now legitimately surfaced (#4954)
  and mentions the locked stream by design.
This commit is contained in:
can1357
2026-07-21 00:16:45 +02:00
parent aa1b89db49
commit 89d6a8f6d1
4 changed files with 16 additions and 8 deletions
@@ -35,6 +35,8 @@ describe("print-mode error exit disposes the session before exit", () => {
const session = {
extensionRunner: undefined,
subscribe: () => {},
settings: { get: () => false },
sessionManager: { buildSessionContext: () => ({ messages: [] }), getEntries: () => [] },
state: { messages: [errorMsg] },
getLastAssistantMessage: () => errorMsg,
prepareForHeadlessAdvisorDrain: () => {},
@@ -115,14 +115,12 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
// the first prompt; persisting the mode_change also lets a later interactive
// attachment restore and review the generated plan.
let abortAfterPlanProposal = false;
const hasConversationContext = session.sessionManager.buildSessionContext().messages.length > 0;
const hasExplicitMode = session.sessionManager.getEntries().some(entry => entry.type === "mode_change");
if (
!hasConversationContext &&
!hasExplicitMode &&
const planDefaultArmed =
session.settings.get("plan.defaultOnStartup") &&
session.settings.get("plan.enabled")
) {
session.settings.get("plan.enabled") &&
session.sessionManager.buildSessionContext().messages.length === 0 &&
!session.sessionManager.getEntries().some(entry => entry.type === "mode_change");
if (planDefaultArmed) {
const planFilePath = session.getPlanReferencePath() || "local://PLAN.md";
const previousTools = session.getEnabledToolNames();
const planTools = session.hasBuiltInTool("write") ? [...new Set([...previousTools, "write"])] : previousTools;
@@ -46,7 +46,12 @@ async function expectRpcModeOwnsStdin(mode: "rpc" | "rpc-ui"): Promise<void> {
}
const stderr = await stderrPromise;
expect(stderr).not.toContain("ReadableStream is locked");
// The adversarial fixture is EXPECTED to fail loading — RPC claimed stdin
// first — and its surfaced load notice (#4954) mentions the locked stream.
// Any OTHER "ReadableStream is locked" line means RPC lost stdin ownership.
for (const line of stderr.split("\n").filter(l => l.includes("ReadableStream is locked"))) {
expect(line).toContain("Failed to load extension");
}
expect(stateResponse?.success).toBe(true);
}
@@ -45,8 +45,11 @@ function createMockSession(
return {
state: { messages },
getLastAssistantMessage: () => messages.findLast(message => message.role === "assistant"),
settings: { get: () => false },
sessionManager: {
getHeader: () => undefined,
buildSessionContext: () => ({ messages: [] }),
getEntries: () => [],
},
extensionRunner: undefined,
subscribe: () => () => {},