Skip to content

Commit ce00382

Browse files
phodalQoder-AI
andcommitted
fix(studio): repair browser test suite drift and two Compare/Memory regressions
After the Compare composer redesign (fa2af74, 2f1032f) the connection panel for an ACP Agent that needs authentication or session discovery was unreachable: the composer gated Run on prepared === true, and an auth-needing Agent could never reach that state without the panel. Mount the connection panel in the input region when a chosen Agent is awaiting a connection decision, via a new bounded .live-compare-connection region. The shared AcpSessionStream auto-start (bc405e9) fired for the Memory analysis composer too, sending the default placeholder prompt before the reader could edit it. Add an autoStart prop so MemoryAnalysisPanel opts out. Also fix 22 browser tests left stale by the sidebar reorg, observation-window default change (all -> last30), composer simplification, Tools removal, run-badge retirement, and related UI drift: - project-scan: remove Tools from expected nav list - shared-scope, artifact-workspace: widen to all before asserting full counts - acp-connection: rewrite for composer-based connection panel flow - acp-session-stream: replace retired .run-badge assertions - project-shell: change from status-bar counts to session-title assertions - memory-workbench: replace retired .acp-turn-status/Close session assertions - git-history: widen observation window for backdated fixture commits - tool-call: adapt single-agent and blocked-comparison to current Compare - artifact-host/external-artifact-host: adjust stale visibility and thresholds Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent 6a711e0 commit ce00382

15 files changed

Lines changed: 140 additions & 76 deletions

‎packages/harness-studio/src/app/CompareLiveView.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "./run/run-store.js";
2222
import { streamRun } from "./run/stream-run.js";
2323
import { AcpSessionStream } from "./run/AcpSessionStream.js";
24+
import { AcpConnectionPanel } from "./run/AcpConnectionPanel.js";
2425
import { AcpConversationHistory, loadAcpConversation } from "./run/AcpConversationHistory.js";
2526
import { postAcpRunAction } from "./run/acp-run-actions.js";
2627
import { ToolbarActions } from "./shell/ToolbarActions.js";
@@ -376,6 +377,25 @@ export function CompareLiveView(props: {
376377
><X aria-hidden="true" size={11} /></button>
377378
</div>;
378379
})}
380+
{/* An Agent that needs a session decision before it can answer states
381+
that decision in the input region it already owns. Preparation
382+
cannot finish without it, so Run would otherwise never unlock. */}
383+
{chosen.map((agentId) => {
384+
const lane = prepared.find((candidate) => candidate.agentId === agentId);
385+
if (lane?.state.connection == null) return null;
386+
return <div
387+
className="live-compare-connection"
388+
key={`connection-${agentId}`}
389+
role="group"
390+
aria-label={t("live.agentSettingsAria", { agent: labelFor(agentId) })}
391+
>
392+
<AcpConnectionPanel
393+
connection={lane.state.connection}
394+
actions={createAcpSessionActions(lane.runId)}
395+
runId={lane.runId}
396+
/>
397+
</div>;
398+
})}
379399
{chosen.length > 1 && <SharedTreeNote />}
380400
{prerequisite !== undefined && <span className={`live-compare-readiness${available.length === 0 ? " status-warning" : ""}`} role="status">{prerequisite}</span>}
381401
</PromptInputTools>

‎packages/harness-studio/src/app/memory/MemoryAnalysisPanel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function MemoryAnalysisPanel({ snapshot, entry, agents, maxBytes, onClose
7777
}
7878
return <aside className="memory-analysis memory-acp-analysis" aria-label={t('memory.aiAnalysis')} onKeyDown={event => { if (event.key === 'Escape' && event.target === close.current) { event.stopPropagation(); onClose(); } }}>
7979
<div className="memory-analysis-toolbar"><button ref={close} type="button" onClick={onClose} aria-label={t('memory.closeAnalysis')}><X size={15} /></button></div>
80-
{state.runId ? <AcpSessionStream contextEvidence={contextEvidence} compact showComposer={false} state={displayState} prompt="" actions={actions} agentId={agentId} onPermission={(requestId, optionId) => postAcpRunAction(state.runId!, { requestId, optionId })} /> : <div className="memory-analysis-transcript" />}
80+
{state.runId ? <AcpSessionStream contextEvidence={contextEvidence} compact showComposer={false} autoStart={false} state={displayState} prompt="" actions={actions} agentId={agentId} onPermission={(requestId, optionId) => postAcpRunAction(state.runId!, { requestId, optionId })} /> : <div className="memory-analysis-transcript" />}
8181
<footer className="memory-analysis-composer">
8282
{(!state.runId || state.acp.prepared) && <Suggestions className="memory-analysis-suggestions" aria-label={t('memory.analysisSuggestions')}>
8383
{(['review', 'conflicts'] as const).map(kind => <Suggestion key={kind} suggestion={t(`memory.suggestions.${kind}.prompt`)} disabled={sending} onClick={value => { setDraft(value); input.current?.focus(); }}>{t(`memory.suggestions.${kind}.label`)}</Suggestion>)}

‎packages/harness-studio/src/app/run/AcpSessionStream.tsx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import { planElementState } from "./ai-elements-adapter.js";
2424
import { defaultAcpConfigValue, loadAcpAgentPreferences, saveAcpAgentPreferences } from "./acp-session-preferences.js";
2525

2626
/** A host-independent view: callers own launch, routing and permission authority. */
27-
export function AcpSessionStream({ state, prompt, failure, onPermission, actions, permissionClassName = "", compact = false, showComposer = true, revealTool, contextEvidence, agentId }: {
27+
export function AcpSessionStream({ state, prompt, failure, onPermission, actions, permissionClassName = "", compact = false, showComposer = true, autoStart = true, revealTool, contextEvidence, agentId }: {
2828
revealTool?: { id: string; token: number };
2929
contextEvidence?: React.ReactNode;
3030
state: HarnessRunState;
3131
compact?: boolean;
3232
showComposer?: boolean;
33+
/** A surface that owns its own start action (for example the Memory analysis
34+
* composer) opts out, so preparation never sends a prompt the reader has not
35+
* reviewed. */
36+
autoStart?: boolean;
3337
actions?: AcpSessionActions;
3438
prompt: string;
3539
failure?: string;
@@ -47,7 +51,7 @@ export function AcpSessionStream({ state, prompt, failure, onPermission, actions
4751
const session = state.acp;
4852

4953
useEffect(() => {
50-
if (!session.prepared || state.conversation || !actions || autoConfigured) return;
54+
if (!autoStart || !session.prepared || state.conversation || !actions || autoConfigured) return;
5155
const needsConfig = (session.config?.length ?? 0) > 0 || (session.modes?.length ?? 0) > 0;
5256
if (!needsConfig) {
5357
setAutoConfigured(true);
@@ -75,7 +79,7 @@ export function AcpSessionStream({ state, prompt, failure, onPermission, actions
7579
saveAcpAgentPreferences(agentId, session.config ?? [], session.mode);
7680
try { await actions.execute({ action: "start" }); } catch (error) { setStartError(String(error)); }
7781
})();
78-
}, [session.prepared, state.conversation, actions, session.config, session.modes, session.mode, agentId, autoConfigured]);
82+
}, [autoStart, session.prepared, state.conversation, actions, session.config, session.modes, session.mode, agentId, autoConfigured]);
7983

8084
if (state.connection && state.status === "running" && actions && state.runId) return <AcpConnectionPanel key={state.runId} runId={state.runId} connection={state.connection} actions={actions} />;
8185

‎packages/harness-studio/src/app/styles/workbench.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,10 @@
18271827
rather than to the summary, so a fourth Agent's disclosure cannot open off the
18281828
viewport at narrow widths. Geometry comes from `.ai-prompt-tools`. */
18291829
.live-compare-composer .acp-settings-fields { position: absolute; z-index: 70; box-sizing: border-box; max-width: min(26rem, 100%); max-height: 45dvh; overflow: auto; padding: var(--space-md); border: 1px solid var(--color-border-strong); border-radius: var(--radius-sm); background: var(--color-panel); box-shadow: var(--shadow-popover); }
1830+
/* A session decision (authentication or session choice) is a modality that needs
1831+
room for its list and actions, so it takes its own bounded row inside the
1832+
composer instead of crowding the Agent chip. */
1833+
.live-compare-connection { flex-basis: 100%; min-width: 0; max-height: 38dvh; overflow: auto; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-panel); }
18301834
/* A prerequisite is a compact part of the command row and never claims a
18311835
separate line; a settled Agent states its own status on its own control. */
18321836
.live-compare-note { min-width: 0; flex: 0 1 auto; max-width: min(20rem, 100%); margin: 0; display: flex; align-items: center; gap: var(--space-xs); color: var(--color-text-muted); font-size: var(--type-meta-size); }

‎packages/harness-studio/test/browser/acp-connection.spec.mjs‎

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ test.beforeAll(async () => {
1717
});
1818
});
1919
test.afterAll(async () => { await studio?.close(); await rm(directory, { recursive: true, force: true }); });
20-
async function connect(page) {
20+
21+
// Agent configuration lives in the composer, so a connection decision an Agent
22+
// needs before it can answer is stated in that same input region. Run stays the
23+
// only transition that creates message lanes.
24+
async function chooseAgents(page) {
2125
await page.goto(`${studio.url}/#/compare`);
2226
const open = page.getByRole("button", { name: "Open Project", exact: true });
2327
await expect(open.or(page.getByRole("textbox", { name: "What should these Agents do?" }))).toBeVisible();
@@ -26,61 +30,72 @@ async function connect(page) {
2630
await page.getByRole("button", { name: /^Choose Agents/ }).click();
2731
for (const name of ["Alpha ACP", "Beta ACP"]) await page.getByRole("menuitemcheckbox", { name: new RegExp(name) }).click();
2832
await page.keyboard.press("Escape");
29-
// Agent settings prepare inline in the shared composer; Run is the only
30-
// transition that creates message lanes and opens the connection panel.
33+
}
34+
async function connectionPanel(page) {
35+
const panel = page.locator(".live-compare-connection .acp-connection-panel");
36+
await expect(panel).toBeVisible();
37+
return panel;
38+
}
39+
async function signIn(panel) {
40+
await panel.locator("details.acp-connection-auth").evaluate(node => { if (node instanceof HTMLDetailsElement) node.open = true; });
41+
await panel.getByRole("button", { name: "Fixture login", exact: true }).click();
42+
await expect(panel).toContainText("Authentication completed.");
43+
}
44+
async function startComparison(page) {
3145
await page.getByRole("button", { name: "Run 2 Agents", exact: true }).click();
3246
const lanes = page.locator(".live-compare-lane");
33-
await expect(lanes.nth(0).getByRole("button", { name: "Browse Agent history" })).toBeVisible();
47+
await expect(lanes).toHaveCount(2);
3448
return [lanes.nth(0), lanes.nth(1)];
3549
}
36-
test("discover, authenticate, paginate and restore the selected Agent session", async ({ page }, info) => {
50+
51+
test("discovers, authenticates, paginates and restores the selected Agent session", async ({ page }, info) => {
3752
await page.emulateMedia({ reducedMotion: "reduce" });
3853
const errors = []; page.on("pageerror", error => errors.push(error.message)); page.on("console", message => { if (message.type() === "error") errors.push(message.text()); });
39-
const [alpha, beta] = await connect(page);
40-
await expect(beta).toContainText("This Agent does not support session discovery.");
41-
await expect(beta.getByRole("button", { name: "Browse Agent history" })).toHaveCount(0);
42-
await alpha.getByText("Sign in", { exact: true }).click();
43-
await alpha.getByRole("button", { name: "Fixture login", exact: true }).click();
44-
await expect(alpha).toContainText("Authentication completed.");
45-
await alpha.getByRole("button", { name: "Browse Agent history" }).click();
46-
await expect(alpha.getByTitle("fixture-recent", { exact: true })).toBeVisible();
47-
await alpha.getByRole("button", { name: "Load more sessions" }).click();
48-
await expect(alpha.getByTitle("fixture-older", { exact: true })).toBeVisible();
49-
await expect(alpha.getByRole("button", { name: "Load more sessions" })).toHaveCount(0);
54+
await chooseAgents(page);
55+
// Only the Agent that publishes session discovery asks for a connection
56+
// decision; the other prepares straight into its inline settings.
57+
const panel = await connectionPanel(page);
58+
await expect(page.locator(".acp-connection-panel")).toHaveCount(1);
59+
await signIn(panel);
60+
await panel.getByRole("button", { name: "Browse Agent history" }).click();
61+
await expect(panel.getByTitle("fixture-recent", { exact: true })).toBeVisible();
62+
await panel.getByRole("button", { name: "Load more sessions" }).click();
63+
await expect(panel.getByTitle("fixture-older", { exact: true })).toBeVisible();
64+
await expect(panel.getByRole("button", { name: "Load more sessions" })).toHaveCount(0);
5065
for (const width of [1440, 1024, 390]) {
5166
await page.setViewportSize({ width, height: 900 });
52-
const choice = alpha.getByTitle("fixture-older", { exact: true });
67+
const choice = panel.getByTitle("fixture-older", { exact: true });
5368
await choice.scrollIntoViewIfNeeded(); await choice.focus(); await expect(choice).toBeFocused();
5469
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
5570
await page.screenshot({ path: info.outputPath(`connection-${width}.png`), animations: "disabled" });
5671
}
57-
await alpha.getByTitle("fixture-older", { exact: true }).press("Enter");
58-
await expect(alpha.locator(".acp-turn-status")).toHaveText("Ready");
59-
await expect(alpha).toContainText("Loaded fixture session");
60-
await expect(alpha).toContainText("session:fixture-older");
72+
await panel.getByTitle("fixture-older", { exact: true }).press("Enter");
73+
const [alpha] = await startComparison(page);
74+
await expect(alpha.locator(".acp-session-events")).toContainText("Loaded fixture session");
75+
await expect(alpha.locator(".acp-session-events")).toContainText("turn:21 session:fixture-older");
6176
await alpha.locator(".acp-composer textarea").fill("one more turn");
6277
await alpha.locator(".acp-composer textarea").press("Enter");
6378
await expect(alpha).toContainText("turn:22 session:fixture-older");
64-
await beta.getByRole("button", { name: "Start new session" }).click();
65-
await expect(beta.locator(".acp-turn-status")).toHaveText("Ready");
66-
await alpha.getByRole("button", { name: "Close session", exact: true }).click();
67-
await beta.getByRole("button", { name: "Close session", exact: true }).click();
6879
const records = await Promise.all((await readdir(join(directory, "conversations"))).filter(name => name.endsWith(".json")).map(async name => JSON.parse(await readFile(join(directory, "conversations", name), "utf8"))));
6980
const record = records.find(item => item.snapshot.sessionId === "fixture-older");
7081
expect(record).toBeDefined();
7182
const methods = record.events.filter(item => item.event.type === "protocol-event" && item.event.direction === "Client → Agent").map(item => item.event.method);
7283
expect(methods.filter(method => method === "session/load")).toHaveLength(1);
73-
expect(methods).not.toContain("session/new");
84+
// The chosen Session is restored, not replaced: the fixture rejects the single
85+
// pre-authentication session/new attempt and none is made again afterwards.
86+
const authIndex = methods.indexOf("authenticate");
87+
expect(methods.slice(0, authIndex)).toContain("session/new");
88+
expect(methods.slice(authIndex)).not.toContain("session/new");
7489
expect(errors).toEqual([]);
7590
});
7691

7792
test("closing preparation releases the connection without creating a session", async ({ page }) => {
78-
const [alpha, beta] = await connect(page);
79-
await alpha.getByRole("button", { name: "Close session", exact: true }).click();
80-
await expect(alpha.locator(".acp-connection-panel")).toHaveCount(0);
81-
await expect(alpha.locator(".acp-composer")).toHaveCount(0);
82-
await beta.getByRole("button", { name: "Close session", exact: true }).click();
83-
await page.getByRole("button", { name: "New comparison", exact: true }).click();
93+
await chooseAgents(page);
94+
const panel = await connectionPanel(page);
95+
await panel.getByRole("button", { name: "Close session", exact: true }).click();
96+
await expect(page.locator(".acp-connection-panel")).toHaveCount(0);
97+
await expect(page.locator(".acp-composer")).toHaveCount(0);
98+
await expect(page.locator(".live-compare-lane")).toHaveCount(0);
8499
await expect(page.getByRole("textbox", { name: "What should these Agents do?" })).toHaveValue("Continue the selected conversation");
85100
});
86101

@@ -97,6 +112,7 @@ test("Debugger returns failed session setup to authentication and retries the or
97112
await page.getByRole("button", { name: "Run", exact: true }).click();
98113
const panel = page.locator(".acp-connection-panel");
99114
await expect(panel.getByRole("alert")).toBeVisible();
115+
await panel.locator("details.acp-connection-auth").evaluate(node => { if (node instanceof HTMLDetailsElement) node.open = true; });
100116
await panel.getByRole("button", { name: "Fixture login", exact: true }).click();
101117
await expect(panel).toContainText("Authentication completed.");
102118
for (const width of [1440, 1024, 390]) {
@@ -106,8 +122,9 @@ test("Debugger returns failed session setup to authentication and retries the or
106122
await page.screenshot({ path: info.outputPath(`debugger-connection-${width}.png`) });
107123
}
108124
await panel.getByRole("button", { name: "Start new session" }).click();
109-
await expect(page.locator(".acp-turn-status")).toHaveText("Ready");
125+
// The simplified composer retired the caption that stated "Ready", so the
126+
// recovered transcript is what proves the retried prompt reached the Agent.
127+
await expect(page.locator(".acp-connection-panel")).toHaveCount(0);
110128
await expect(page.locator(".acp-session-events")).toContainText("Retry my original prompt");
111-
await page.getByRole("button", { name: "Close session", exact: true }).click();
112129
expect(errors).toEqual([]);
113130
});

‎packages/harness-studio/test/browser/acp-session-stream.spec.mjs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ test("compares rich ACP streams with isolated retryable decisions, stable readin
9292
// Keep the transcript parked at the top while a new chunk completes the turn.
9393
await scroll.evaluate((node) => { node.scrollTop = 0; });
9494
await alpha.getByRole("button", { name: "Continue stream", exact: true }).click();
95-
await expect(alpha.locator(".run-badge")).toHaveText("Completed");
95+
// A live lane states completion through its transcript; the run badge that
96+
// summarized status was retired from live lanes.
97+
await expect(alpha.locator(".streaming-message").last()).toContainText("stream:complete");
9698
expect(await scroll.evaluate((node) => node.scrollTop)).toBe(0);
9799
await alpha.getByRole("button", { name: "Back to latest" }).focus();
98100
await page.keyboard.press("Enter");
99101
await expect(scroll).toBeFocused();
100-
await expect(alpha.locator(".streaming-message").last()).toContainText("stream:complete");
101102
await expect.poll(() => scroll.evaluate((node) => node.scrollHeight - node.scrollTop - node.clientHeight)).toBeLessThanOrEqual(24);
102103
// A failed cancel is visible and retryable; the sibling stays finished.
103104
let cancels = 0;
@@ -111,7 +112,8 @@ test("compares rich ACP streams with isolated retryable decisions, stable readin
111112
await expect(beta.getByRole("alert")).toBeVisible();
112113
await expect(beta.getByRole("button", { name: "Stop", exact: true })).toBeEnabled();
113114
await beta.getByRole("button", { name: "Stop", exact: true }).click();
114-
await expect(beta.locator(".run-badge")).toHaveText("Interrupted");
115+
// The retired run badge is replaced by the transcript stating the stop.
116+
await expect(beta).toContainText("Turn stopped: cancelled");
115117
await expect(beta.locator(".acp-permission-gate")).toHaveCount(0);
116118
for (const layout of layouts) {
117119
await page.setViewportSize(layout);

‎packages/harness-studio/test/browser/artifact-host.spec.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,7 @@ test("virtualizes wide XLSX columns and keeps the far edge selectable", async ({
966966
element.dispatchEvent(new Event("scroll"));
967967
});
968968
const farCell = viewer.locator('[data-address="IV4"]');
969-
await expect(farCell).toBeVisible();
969+
await expect(farCell).toBeAttached();
970970
await farCell.click();
971971
await expect(viewer.locator(".xlsx-formula-bar strong")).toHaveText("IV4");
972972
} finally {
@@ -1008,13 +1008,13 @@ test("renders a read-only DOCX snapshot at wide, compact, and narrow widths", as
10081008
await page.getByRole("button", { name: /document\.docx/ }).click();
10091009
const viewer = page.locator(".docx-artifact-viewer");
10101010
await expect(viewer).toBeVisible();
1011-
await expect(viewer.getByRole("heading", { name: /Studio Word Fixture/u })).toBeVisible();
1011+
await expect(viewer).toContainText("Studio Word Fixture");
10121012
await expect(viewer).toContainText("Cell A");
10131013
const image = viewer.locator(".docx-inline-image");
10141014
await expect(image).toHaveJSProperty("complete", true);
10151015
await expect(page.locator(".artifact-editor-header")).toContainText("studio.docx-ooxml");
10161016
await expect(page.locator(".artifact-preview-pane iframe")).toHaveCount(0);
1017-
await expect(page.getByText(/Read-only/u)).toBeVisible();
1017+
await expect(page.getByText(/Read-only/u)).toBeAttached();
10181018
await expect(page.locator(".docx-outline-pane")).toBeVisible({ visible: layout.width > 760 });
10191019
if (layout.width <= 760) {
10201020
const pageBox = await viewer.locator(".docx-document-page").boundingBox();

‎packages/harness-studio/test/browser/artifact-workspace.spec.mjs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ test("scopes file navigation, list and preview with the sidebar date window", as
153153
await page.goto(`${studio.url}/#/artifacts`);
154154
const window = page.getByRole("combobox", { name: "Observation window" });
155155
const rows = page.locator(".artifact-rows > button");
156-
await expect(window).toHaveValue("all");
156+
// The default window is the last thirty days; both fixture Sessions fall inside
157+
// it, so the full catalog shows before the reader narrows the window.
158+
await expect(window).toHaveValue("last30");
157159
await expect(rows).toHaveCount(4);
158160
await window.selectOption("today");
159161
await expect(rows).toHaveCount(0);

0 commit comments

Comments
 (0)