Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
814 lines
27 KiB
JavaScript
814 lines
27 KiB
JavaScript
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
clickAgentWindowControl,
|
|
clickProviderTarget,
|
|
clickSession,
|
|
evaluate,
|
|
finishRendererScenario,
|
|
markRenderer,
|
|
selectProvider,
|
|
startRendererScenario,
|
|
waitForActiveSession,
|
|
waitForAgentWorkbenchWindow,
|
|
waitForEvaluation,
|
|
waitForProviderTiles,
|
|
waitForSelectedTarget,
|
|
waitForStableAgentWorkbenchWindow,
|
|
waitForStableRail,
|
|
waitForStableViewport
|
|
} from "./agent-gui-performance-helpers.mjs";
|
|
import {
|
|
requiredScenarioData,
|
|
scenarioSummary as summary,
|
|
sqlString,
|
|
startupWorkspaceID,
|
|
updateAgentGUISnapshot
|
|
} from "./agent-gui-performance-snapshot-helpers.mjs";
|
|
|
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
const cursorFixtureDirectory = join(
|
|
scriptDirectory,
|
|
"fixtures",
|
|
"agent-gui-performance"
|
|
);
|
|
const conversationItemPrefix = "agent-gui-conversation-item-";
|
|
|
|
const virtualizedStreamingMarkers = {
|
|
start: "tutti-perf:virtualized-streaming:start",
|
|
submitted: "tutti-perf:virtualized-streaming:submitted-observed",
|
|
firstMutation: "tutti-perf:virtualized-streaming:first-mutation-observed",
|
|
settled: "tutti-perf:virtualized-streaming:settled-observed",
|
|
end: "tutti-perf:virtualized-streaming:end"
|
|
};
|
|
|
|
export const virtualizedStreamingScenario = {
|
|
id: "virtualized-streaming",
|
|
markers: virtualizedStreamingMarkers,
|
|
milestones: [
|
|
{
|
|
key: "submitted",
|
|
label: "fixture prompt submitted",
|
|
marker: virtualizedStreamingMarkers.submitted
|
|
},
|
|
{
|
|
key: "firstMutation",
|
|
label: "first virtualized transcript mutation",
|
|
marker: virtualizedStreamingMarkers.firstMutation
|
|
},
|
|
{
|
|
key: "settled",
|
|
label: "streaming fixture settled",
|
|
marker: virtualizedStreamingMarkers.settled
|
|
}
|
|
],
|
|
prepareSnapshot: prepareVirtualizedTranscriptSnapshot,
|
|
prepare: prepareVirtualizedStreaming,
|
|
execute: executeVirtualizedStreaming,
|
|
describe(prepared) {
|
|
return `${prepared.sessionID}; ${prepared.turnCount} persisted turns + deterministic ACP stream`;
|
|
},
|
|
summarize(prepared, result) {
|
|
return summary(
|
|
[
|
|
{
|
|
name: "transcript virtualized before stream",
|
|
passed: prepared.virtualized
|
|
},
|
|
{
|
|
name: "stream entered working state",
|
|
passed: result.started
|
|
},
|
|
{
|
|
name: "virtualizer DOM mutated repeatedly",
|
|
passed: result.mutationBatches >= 8
|
|
},
|
|
{
|
|
name: "transcript remained virtualized",
|
|
passed: result.virtualizedAfter
|
|
},
|
|
{ name: "stream settled", passed: result.settled }
|
|
],
|
|
[
|
|
{ label: "Session", value: prepared.sessionID },
|
|
{ label: "Persisted turns", value: String(prepared.turnCount) },
|
|
{ label: "Mutation batches", value: String(result.mutationBatches) },
|
|
{ label: "DOM mutations", value: String(result.mutations) },
|
|
{ label: "Provider", value: "isolated fake Cursor ACP" }
|
|
],
|
|
"virtualized DOM is observed before submit; deterministic local ACP chunks produce at least eight MutationObserver batches; working state settles before the trace tail"
|
|
);
|
|
}
|
|
};
|
|
|
|
export async function prepareVirtualizedTranscriptSnapshot(
|
|
context,
|
|
options = {}
|
|
) {
|
|
const fixtureEnvironment = await installCursorPerformanceFixture(context);
|
|
const workspaceID = await startupWorkspaceID(context);
|
|
const richTextCandidateRequirement = options.richTextFixture
|
|
? "AND userTextMessageCount >= 4"
|
|
: "";
|
|
const candidates = await context.sqliteJSON(
|
|
context.databasePath,
|
|
`
|
|
SELECT s.agent_session_id AS sessionID,
|
|
COUNT(t.turn_id) AS turnCount,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM workspace_agent_messages m
|
|
WHERE m.workspace_id = s.workspace_id
|
|
AND m.agent_session_id = s.agent_session_id
|
|
AND m.deleted_at_unix_ms = 0
|
|
AND m.role = 'user'
|
|
AND m.kind = 'text'
|
|
) AS userTextMessageCount
|
|
FROM workspace_agent_sessions s
|
|
JOIN workspace_agent_turns t
|
|
ON t.workspace_id = s.workspace_id
|
|
AND t.agent_session_id = s.agent_session_id
|
|
WHERE s.workspace_id = '${sqlString(workspaceID)}'
|
|
AND s.deleted_at_unix_ms = 0
|
|
AND s.origin = 'WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME'
|
|
AND s.session_kind = 'root'
|
|
AND s.active_turn_id IS NULL
|
|
GROUP BY s.agent_session_id
|
|
HAVING COUNT(t.turn_id) >= 30
|
|
${richTextCandidateRequirement}
|
|
ORDER BY COUNT(t.turn_id) ASC, s.agent_session_id ASC
|
|
LIMIT 1;
|
|
`
|
|
);
|
|
const candidate = candidates[0];
|
|
if (!candidate?.sessionID) {
|
|
throw new Error(
|
|
options.richTextFixture
|
|
? "virtualized-scroll-locator requires one root session with at least 30 settled turns and four user text messages in the source snapshot"
|
|
: "virtualized-streaming requires one root session with at least 30 settled turns in the source snapshot"
|
|
);
|
|
}
|
|
const now = Date.now();
|
|
const fixtureMention = `[@Cursor](mention://agent-target/local%3Acursor?workspaceId=${encodeURIComponent(workspaceID)})`;
|
|
const richTextFixture = [
|
|
"Virtualized transcript performance fixture",
|
|
`${fixtureMention} inspect the historical scroll anchor and preserve the visible turn`,
|
|
"Paragraph three contains enough deterministic text to produce stable wrapping across narrow and wide transcript viewports",
|
|
"Paragraph four exercises static rich-text document creation without mounting an editable ProseMirror view",
|
|
`${fixtureMention} compare the current viewport with the message locator while older turns enter the virtual window`,
|
|
"Paragraph six keeps each historical user message tall enough to sustain a long monotonic upward scroll",
|
|
"Paragraph seven verifies that repeated line boundaries remain static while the virtualizer replaces mounted turns",
|
|
`${fixtureMention} finish the fixture with a third mention chip and a final wrapped paragraph`
|
|
].join("\n");
|
|
const richTextFixtureUpdate = options.richTextFixture
|
|
? `UPDATE workspace_agent_messages
|
|
SET payload_json = json_set(
|
|
payload_json,
|
|
'$.text',
|
|
'${sqlString(richTextFixture)}'
|
|
),
|
|
updated_at_unix_ms = ${now}
|
|
WHERE workspace_id = '${sqlString(workspaceID)}'
|
|
AND agent_session_id = '${sqlString(candidate.sessionID)}'
|
|
AND deleted_at_unix_ms = 0
|
|
AND role = 'user'
|
|
AND kind = 'text';`
|
|
: "";
|
|
await context.sqliteExec(
|
|
context.databasePath,
|
|
`
|
|
PRAGMA foreign_keys = ON;
|
|
UPDATE agent_targets
|
|
SET enabled = 1, updated_at_ms = ${now}
|
|
WHERE id = 'local:cursor';
|
|
UPDATE workspace_agent_sessions
|
|
SET agent_target_id = 'local:cursor',
|
|
provider = 'cursor',
|
|
provider_session_id = 'tutti-perf-cursor-session',
|
|
model = '',
|
|
settings_json = '{}',
|
|
cwd = '${sqlString(context.workspaceRoot)}',
|
|
rail_section_kind = 'conversations',
|
|
rail_project_path = '',
|
|
rail_section_key = 'conversations',
|
|
session_metadata_json = json_set(
|
|
session_metadata_json,
|
|
'$.visible', json('true'),
|
|
'$.imported', json('false')
|
|
),
|
|
internal_runtime_context_json = '{}',
|
|
updated_at_unix_ms = ${now}
|
|
WHERE workspace_id = '${sqlString(workspaceID)}'
|
|
AND agent_session_id = '${sqlString(candidate.sessionID)}';
|
|
${richTextFixtureUpdate}
|
|
`
|
|
);
|
|
return {
|
|
data: {
|
|
sessionID: candidate.sessionID,
|
|
turnCount: Number(candidate.turnCount),
|
|
richTextMessageCount: Number(candidate.userTextMessageCount),
|
|
richTextMentionsPerMessage: options.richTextFixture ? 3 : 0,
|
|
richTextParagraphsPerMessage: options.richTextFixture ? 8 : 0,
|
|
workspaceID
|
|
},
|
|
environment: fixtureEnvironment
|
|
};
|
|
}
|
|
|
|
export async function installCursorPerformanceFixture(context) {
|
|
const fixtureBinDirectory = join(context.runtimeDirectory, "state", "bin");
|
|
await mkdir(fixtureBinDirectory, { recursive: true });
|
|
const fixtureBinaryPath = join(fixtureBinDirectory, "cursor-agent");
|
|
await copyFile(
|
|
join(cursorFixtureDirectory, "cursor-agent"),
|
|
fixtureBinaryPath
|
|
);
|
|
await chmod(fixtureBinaryPath, 0o755);
|
|
return {
|
|
PATH: `${fixtureBinDirectory}:${process.env.PATH ?? ""}`,
|
|
SHELL: fixtureBinaryPath
|
|
};
|
|
}
|
|
|
|
async function prepareVirtualizedStreaming(context, options) {
|
|
return prepareVirtualizedTranscript(
|
|
context,
|
|
options,
|
|
"virtualized-streaming"
|
|
);
|
|
}
|
|
|
|
export async function prepareVirtualizedTranscript(
|
|
context,
|
|
options,
|
|
scenarioID
|
|
) {
|
|
const fixture = requiredScenarioData(context, scenarioID);
|
|
const providers = await waitForProviderTiles(
|
|
context.pageClient,
|
|
options.timeoutMs
|
|
);
|
|
if (
|
|
!providers.tiles.some(
|
|
(tile) => tile.targetID === "local:cursor" && !tile.disabled
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`enabled local:cursor target is unavailable; visible targets: ${providers.tiles.map((tile) => `${tile.targetID}${tile.disabled ? " (disabled)" : ""}`).join(", ")}`
|
|
);
|
|
}
|
|
await selectProvider(context.pageClient, "local:cursor", options.timeoutMs);
|
|
await clickSession(context.pageClient, fixture.sessionID);
|
|
await waitForActiveSession(
|
|
context.pageClient,
|
|
fixture.sessionID,
|
|
options.timeoutMs
|
|
);
|
|
const transcript = await waitForEvaluation(
|
|
context.pageClient,
|
|
`(() => {
|
|
const timeline = document.querySelector('[data-testid="agent-gui-timeline"]');
|
|
const virtualized = timeline?.querySelector('[data-agent-transcript-virtualized="true"]');
|
|
const editor = document.querySelector('[data-testid="agent-gui-composer-editor"]');
|
|
return {
|
|
ready: Boolean(timeline && virtualized && editor && editor.getAttribute('aria-disabled') !== 'true'),
|
|
virtualized: Boolean(virtualized)
|
|
};
|
|
})()`,
|
|
options.timeoutMs,
|
|
"virtualized transcript and enabled composer"
|
|
);
|
|
return { ...fixture, virtualized: transcript.virtualized };
|
|
}
|
|
|
|
async function executeVirtualizedStreaming(context, _prepared, options) {
|
|
const { pageClient } = context;
|
|
await evaluate(
|
|
pageClient,
|
|
`(() => {
|
|
const root = document.querySelector('[data-agent-transcript-virtualized="true"]');
|
|
if (!(root instanceof HTMLElement)) throw new Error('virtualized transcript is unavailable');
|
|
const state = window.__tuttiPerfVirtualizedStreaming = {
|
|
firstMutationMarked: false,
|
|
mutationBatches: 0,
|
|
mutations: 0
|
|
};
|
|
state.observer = new MutationObserver((records) => {
|
|
state.mutationBatches += 1;
|
|
state.mutations += records.length;
|
|
if (!state.firstMutationMarked) {
|
|
state.firstMutationMarked = true;
|
|
console.timeStamp(${JSON.stringify(virtualizedStreamingMarkers.firstMutation)});
|
|
}
|
|
});
|
|
state.observer.observe(root, {
|
|
attributes: true,
|
|
characterData: true,
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
return true;
|
|
})()`
|
|
);
|
|
await startRendererScenario(pageClient, virtualizedStreamingMarkers.start);
|
|
await enterAndSubmitComposerPrompt(
|
|
pageClient,
|
|
"Render deterministic virtualized streaming performance fixture",
|
|
options.timeoutMs
|
|
);
|
|
const started = await waitForEvaluation(
|
|
pageClient,
|
|
`({ ready: Boolean(document.querySelector('[data-testid="agent-gui-composer-stop-active-turn"]')) })`,
|
|
options.timeoutMs,
|
|
"streaming fixture working state"
|
|
);
|
|
await markRenderer(pageClient, virtualizedStreamingMarkers.submitted);
|
|
const settled = await waitForEvaluation(
|
|
pageClient,
|
|
`(() => {
|
|
const state = window.__tuttiPerfVirtualizedStreaming;
|
|
return {
|
|
ready: Boolean(state?.firstMutationMarked && state.mutationBatches >= 8 && !document.querySelector('[data-testid="agent-gui-composer-stop-active-turn"]')),
|
|
mutationBatches: state?.mutationBatches ?? 0,
|
|
mutations: state?.mutations ?? 0,
|
|
virtualizedAfter: Boolean(document.querySelector('[data-agent-transcript-virtualized="true"]'))
|
|
};
|
|
})()`,
|
|
options.timeoutMs,
|
|
"settled virtualized streaming fixture",
|
|
50
|
|
);
|
|
await markRenderer(pageClient, virtualizedStreamingMarkers.settled);
|
|
await evaluate(
|
|
pageClient,
|
|
`(() => {
|
|
window.__tuttiPerfVirtualizedStreaming?.observer?.disconnect();
|
|
return true;
|
|
})()`
|
|
);
|
|
await finishRendererScenario(pageClient, virtualizedStreamingMarkers.end);
|
|
return {
|
|
mutationBatches: settled.mutationBatches,
|
|
mutations: settled.mutations,
|
|
settled: settled.ready,
|
|
started: started.ready,
|
|
virtualizedAfter: settled.virtualizedAfter
|
|
};
|
|
}
|
|
|
|
const railRevealMarkers = {
|
|
start: "tutti-perf:rail-scope-reveal:start",
|
|
selected: "tutti-perf:rail-scope-reveal:selected-observed",
|
|
revealed: "tutti-perf:rail-scope-reveal:scroll-into-view-observed",
|
|
stable: "tutti-perf:rail-scope-reveal:stable-observed",
|
|
end: "tutti-perf:rail-scope-reveal:end"
|
|
};
|
|
|
|
export const railScopeRevealScenario = {
|
|
id: "rail-scope-reveal",
|
|
markers: railRevealMarkers,
|
|
milestones: [
|
|
{
|
|
key: "selected",
|
|
label: "target Agent scope selected",
|
|
marker: railRevealMarkers.selected
|
|
},
|
|
{
|
|
key: "revealed",
|
|
label: "active session scrollIntoView",
|
|
marker: railRevealMarkers.revealed
|
|
},
|
|
{
|
|
key: "stable",
|
|
label: "target rail stable",
|
|
marker: railRevealMarkers.stable
|
|
}
|
|
],
|
|
prepareSnapshot: prepareRailScopeRevealSnapshot,
|
|
prepare: prepareRailScopeReveal,
|
|
execute: executeRailScopeReveal,
|
|
describe(prepared) {
|
|
return `${prepared.sourceTargetID} -> ${prepared.targetTargetID}; reveal ${prepared.sessionID}`;
|
|
},
|
|
summarize(prepared, result) {
|
|
return summary(
|
|
[
|
|
{ name: "target Agent selected", passed: result.selected },
|
|
{ name: "target session active", passed: result.active },
|
|
{
|
|
name: "target session received scrollIntoView",
|
|
passed: result.revealCalls > 0
|
|
},
|
|
{ name: "rail stable", passed: result.stable }
|
|
],
|
|
[
|
|
{
|
|
label: "Agent scope",
|
|
value: `${prepared.sourceTargetID} → ${prepared.targetTargetID}`
|
|
},
|
|
{ label: "Session", value: prepared.sessionID },
|
|
{ label: "scrollIntoView calls", value: String(result.revealCalls) }
|
|
],
|
|
"a fresh Agent scope restores its mapped active session; the scenario records the exact target row scrollIntoView call before waiting for five stable rail snapshots"
|
|
);
|
|
}
|
|
};
|
|
|
|
async function prepareRailScopeRevealSnapshot(context) {
|
|
const workspaceID = await startupWorkspaceID(context);
|
|
const targets = await context.sqliteJSON(
|
|
context.databasePath,
|
|
`
|
|
SELECT agent_target_id AS targetID, COUNT(*) AS sessionCount
|
|
FROM workspace_agent_sessions
|
|
WHERE workspace_id = '${sqlString(workspaceID)}'
|
|
AND deleted_at_unix_ms = 0
|
|
AND agent_target_id IS NOT NULL
|
|
GROUP BY agent_target_id
|
|
HAVING COUNT(*) >= 2
|
|
ORDER BY COUNT(*) DESC, agent_target_id ASC
|
|
LIMIT 2;
|
|
`
|
|
);
|
|
if (targets.length < 2) {
|
|
throw new Error(
|
|
"rail-scope-reveal requires sessions for at least two Agent targets"
|
|
);
|
|
}
|
|
const targetTargetID = targets[0].targetID;
|
|
const sourceTargetID = targets[1].targetID;
|
|
const sessions = await context.sqliteJSON(
|
|
context.databasePath,
|
|
`
|
|
SELECT agent_session_id AS sessionID
|
|
FROM workspace_agent_sessions
|
|
WHERE workspace_id = '${sqlString(workspaceID)}'
|
|
AND agent_target_id = '${sqlString(targetTargetID)}'
|
|
AND deleted_at_unix_ms = 0
|
|
ORDER BY updated_at_unix_ms DESC, agent_session_id ASC
|
|
LIMIT 1 OFFSET 4;
|
|
`
|
|
);
|
|
const sessionID = sessions[0]?.sessionID;
|
|
if (!sessionID) {
|
|
throw new Error(
|
|
"rail-scope-reveal could not select a fifth target session"
|
|
);
|
|
}
|
|
await updateAgentGUISnapshot(context, (state) => ({
|
|
...state,
|
|
agentTargetId: sourceTargetID,
|
|
lastActiveAgentSessionId: null,
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
...(state.lastActiveAgentSessionIdByAgentTargetId ?? {}),
|
|
[targetTargetID]: sessionID
|
|
}
|
|
}));
|
|
return {
|
|
data: { sessionID, sourceTargetID, targetTargetID, workspaceID }
|
|
};
|
|
}
|
|
|
|
async function prepareRailScopeReveal(context, options) {
|
|
const fixture = requiredScenarioData(context, "rail-scope-reveal");
|
|
const providers = await waitForProviderTiles(
|
|
context.pageClient,
|
|
options.timeoutMs
|
|
);
|
|
const available = new Set(
|
|
providers.tiles
|
|
.filter((tile) => tile.disabled !== true)
|
|
.map((tile) => tile.targetID)
|
|
);
|
|
if (
|
|
!available.has(fixture.sourceTargetID) ||
|
|
!available.has(fixture.targetTargetID)
|
|
) {
|
|
throw new Error(
|
|
`rail-scope-reveal requires enabled targets ${fixture.sourceTargetID} and ${fixture.targetTargetID}`
|
|
);
|
|
}
|
|
if (providers.selectedTargetID !== fixture.sourceTargetID) {
|
|
await selectProvider(
|
|
context.pageClient,
|
|
fixture.sourceTargetID,
|
|
options.timeoutMs
|
|
);
|
|
} else {
|
|
await waitForStableRail(
|
|
context.pageClient,
|
|
fixture.sourceTargetID,
|
|
options.timeoutMs
|
|
);
|
|
}
|
|
return fixture;
|
|
}
|
|
|
|
async function executeRailScopeReveal(context, prepared, options) {
|
|
const { pageClient } = context;
|
|
await evaluate(
|
|
pageClient,
|
|
`(() => {
|
|
const original = Element.prototype.scrollIntoView;
|
|
window.__tuttiPerfRailReveal = { calls: [], original };
|
|
Element.prototype.scrollIntoView = function(options) {
|
|
const testID = this instanceof HTMLElement ? (this.dataset.testid ?? '') : '';
|
|
window.__tuttiPerfRailReveal.calls.push({ options, testID });
|
|
return original.call(this, options);
|
|
};
|
|
return true;
|
|
})()`
|
|
);
|
|
await startRendererScenario(pageClient, railRevealMarkers.start);
|
|
await clickProviderTarget(pageClient, prepared.targetTargetID);
|
|
const selected = await waitForSelectedTarget(
|
|
pageClient,
|
|
prepared.targetTargetID,
|
|
options.timeoutMs
|
|
);
|
|
await markRenderer(pageClient, railRevealMarkers.selected);
|
|
const active = await waitForActiveSession(
|
|
pageClient,
|
|
prepared.sessionID,
|
|
options.timeoutMs
|
|
);
|
|
const revealed = await waitForEvaluation(
|
|
pageClient,
|
|
`(() => {
|
|
const testID = ${JSON.stringify(conversationItemPrefix)} + ${JSON.stringify(prepared.sessionID)};
|
|
const calls = window.__tuttiPerfRailReveal?.calls ?? [];
|
|
return { ready: calls.some((call) => call.testID === testID), revealCalls: calls.filter((call) => call.testID === testID).length };
|
|
})()`,
|
|
options.timeoutMs,
|
|
`scrollIntoView for ${prepared.sessionID}`,
|
|
50
|
|
);
|
|
await markRenderer(pageClient, railRevealMarkers.revealed);
|
|
const stable = await waitForStableRail(
|
|
pageClient,
|
|
prepared.targetTargetID,
|
|
options.timeoutMs
|
|
);
|
|
await markRenderer(pageClient, railRevealMarkers.stable);
|
|
await evaluate(
|
|
pageClient,
|
|
`(() => {
|
|
const state = window.__tuttiPerfRailReveal;
|
|
if (state?.original) Element.prototype.scrollIntoView = state.original;
|
|
return true;
|
|
})()`
|
|
);
|
|
await finishRendererScenario(pageClient, railRevealMarkers.end);
|
|
return {
|
|
active: active.ready,
|
|
revealCalls: revealed.revealCalls,
|
|
selected: selected.ready,
|
|
stable: stable.sectionCount > 0
|
|
};
|
|
}
|
|
|
|
const composerResizeMarkers = {
|
|
start: "tutti-perf:composer-overflow-resize:start",
|
|
narrowed: "tutti-perf:composer-overflow-resize:narrowed-observed",
|
|
overflowing: "tutti-perf:composer-overflow-resize:overflow-observed",
|
|
restored: "tutti-perf:composer-overflow-resize:restored-observed",
|
|
end: "tutti-perf:composer-overflow-resize:end"
|
|
};
|
|
|
|
export const composerOverflowResizeScenario = {
|
|
id: "composer-overflow-resize",
|
|
markers: composerResizeMarkers,
|
|
milestones: [
|
|
{
|
|
key: "narrowed",
|
|
label: "renderer viewport narrowed",
|
|
marker: composerResizeMarkers.narrowed
|
|
},
|
|
{
|
|
key: "overflowing",
|
|
label: "prompt tip layout measurement observed",
|
|
marker: composerResizeMarkers.overflowing
|
|
},
|
|
{
|
|
key: "restored",
|
|
label: "renderer viewport restored",
|
|
marker: composerResizeMarkers.restored
|
|
}
|
|
],
|
|
prepare: prepareComposerOverflowResize,
|
|
execute: executeComposerOverflowResize,
|
|
describe(prepared) {
|
|
return `${prepared.originalWidth}px -> narrow -> ${prepared.originalWidth}px`;
|
|
},
|
|
summarize(prepared, result) {
|
|
return summary(
|
|
[
|
|
{ name: "hero prompt tip present", passed: prepared.promptTipPresent },
|
|
{ name: "viewport narrowed", passed: result.narrowed },
|
|
{
|
|
name: "prompt tip layout measured after resize",
|
|
passed: result.measurementReads > 0
|
|
},
|
|
{ name: "viewport restored", passed: result.restored }
|
|
],
|
|
[
|
|
{ label: "Original viewport", value: `${prepared.originalWidth}px` },
|
|
{ label: "Narrow viewport", value: `${result.narrowWidth}px` },
|
|
{ label: "Prompt tip", value: "hero composer" },
|
|
{
|
|
label: "Layout getter reads",
|
|
value: String(result.measurementReads)
|
|
}
|
|
],
|
|
"AgentGUI Workbench is fullscreen; native scrollWidth/clientWidth getters are transparently counted while renderer resizes drive the prompt-tip ResizeObserver, then viewport metrics are restored"
|
|
);
|
|
}
|
|
};
|
|
|
|
async function prepareComposerOverflowResize(context, options) {
|
|
const { pageClient } = context;
|
|
const providers = await waitForProviderTiles(pageClient, options.timeoutMs);
|
|
const targetIDValue =
|
|
providers.selectedTargetID ?? providers.tiles[0]?.targetID;
|
|
if (!targetIDValue) {
|
|
throw new Error("composer-overflow-resize has no Agent target");
|
|
}
|
|
if (providers.selectedTargetID !== targetIDValue) {
|
|
await clickProviderTarget(pageClient, targetIDValue);
|
|
await waitForSelectedTarget(pageClient, targetIDValue, options.timeoutMs);
|
|
}
|
|
const windowState = await waitForStableAgentWorkbenchWindow(
|
|
pageClient,
|
|
options.timeoutMs
|
|
);
|
|
if (windowState.displayMode !== "fullscreen") {
|
|
await clickAgentWindowControl(
|
|
pageClient,
|
|
windowState.id,
|
|
"agent-gui-window-toggle-display-mode"
|
|
);
|
|
await waitForAgentWorkbenchWindow(
|
|
pageClient,
|
|
options.timeoutMs,
|
|
"windowState?.displayMode === 'fullscreen'",
|
|
"fullscreen AgentGUI Workbench window",
|
|
windowState.id
|
|
);
|
|
}
|
|
const promptTip = await waitForEvaluation(
|
|
pageClient,
|
|
`(() => {
|
|
const tip = document.querySelector('[data-testid="agent-gui-prompt-tip"]');
|
|
return { ready: Boolean(tip), promptTipPresent: Boolean(tip) };
|
|
})()`,
|
|
options.timeoutMs,
|
|
"hero composer prompt tip"
|
|
);
|
|
const viewport = await waitForStableViewport(pageClient, options.timeoutMs);
|
|
return {
|
|
nodeID: windowState.id,
|
|
originalHeight: viewport.height,
|
|
originalWidth: viewport.width,
|
|
promptTipPresent: promptTip.promptTipPresent
|
|
};
|
|
}
|
|
|
|
async function executeComposerOverflowResize(context, prepared, options) {
|
|
const widths = [760, 680, 600, 520];
|
|
let measurement = null;
|
|
await evaluate(
|
|
context.pageClient,
|
|
`(() => {
|
|
const scrollDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollWidth');
|
|
const clientDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth');
|
|
if (!scrollDescriptor?.get || !clientDescriptor?.get) {
|
|
throw new Error('native inline layout getters are unavailable');
|
|
}
|
|
const state = window.__tuttiPerfComposerResize = {
|
|
clientWidthReads: 0,
|
|
scrollWidthReads: 0,
|
|
scrollDescriptor,
|
|
clientDescriptor
|
|
};
|
|
Object.defineProperty(Element.prototype, 'scrollWidth', {
|
|
...scrollDescriptor,
|
|
get() {
|
|
const value = scrollDescriptor.get.call(this);
|
|
if (this instanceof HTMLElement && this.matches('[data-testid="agent-gui-prompt-tip"]')) {
|
|
state.scrollWidthReads += 1;
|
|
}
|
|
return value;
|
|
}
|
|
});
|
|
Object.defineProperty(Element.prototype, 'clientWidth', {
|
|
...clientDescriptor,
|
|
get() {
|
|
const value = clientDescriptor.get.call(this);
|
|
if (this instanceof HTMLElement && this.matches('[data-testid="agent-gui-prompt-tip"]')) {
|
|
state.clientWidthReads += 1;
|
|
}
|
|
return value;
|
|
}
|
|
});
|
|
return true;
|
|
})()`
|
|
);
|
|
await startRendererScenario(context.pageClient, composerResizeMarkers.start);
|
|
for (const width of widths) {
|
|
await context.pageClient.send("Emulation.setDeviceMetricsOverride", {
|
|
deviceScaleFactor: 1,
|
|
height: prepared.originalHeight,
|
|
mobile: false,
|
|
width
|
|
});
|
|
const viewport = await waitForStableViewport(
|
|
context.pageClient,
|
|
options.timeoutMs
|
|
);
|
|
await markRenderer(context.pageClient, composerResizeMarkers.narrowed);
|
|
measurement = await evaluate(
|
|
context.pageClient,
|
|
`(() => {
|
|
const state = window.__tuttiPerfComposerResize;
|
|
return {
|
|
measurementReads: Math.min(
|
|
state?.scrollWidthReads ?? 0,
|
|
state?.clientWidthReads ?? 0
|
|
),
|
|
viewportWidth: window.innerWidth
|
|
};
|
|
})()`
|
|
);
|
|
if (measurement?.measurementReads > 0) {
|
|
measurement.viewportWidth = viewport.width;
|
|
break;
|
|
}
|
|
}
|
|
if (!measurement?.measurementReads) {
|
|
throw new Error(
|
|
"prompt tip layout getters were not read after viewport resize"
|
|
);
|
|
}
|
|
await markRenderer(context.pageClient, composerResizeMarkers.overflowing);
|
|
await context.pageClient.send("Emulation.clearDeviceMetricsOverride");
|
|
const restored = await waitForStableViewport(
|
|
context.pageClient,
|
|
options.timeoutMs
|
|
);
|
|
await evaluate(
|
|
context.pageClient,
|
|
`(() => {
|
|
const state = window.__tuttiPerfComposerResize;
|
|
if (!state) return false;
|
|
Object.defineProperty(Element.prototype, 'scrollWidth', state.scrollDescriptor);
|
|
Object.defineProperty(Element.prototype, 'clientWidth', state.clientDescriptor);
|
|
return true;
|
|
})()`
|
|
);
|
|
await markRenderer(context.pageClient, composerResizeMarkers.restored);
|
|
await finishRendererScenario(context.pageClient, composerResizeMarkers.end);
|
|
return {
|
|
narrowWidth: measurement.viewportWidth,
|
|
narrowed: measurement.viewportWidth < prepared.originalWidth,
|
|
measurementReads: measurement.measurementReads,
|
|
restored: Math.abs(restored.width - prepared.originalWidth) <= 2
|
|
};
|
|
}
|
|
|
|
export async function enterAndSubmitComposerPrompt(client, prompt, timeoutMs) {
|
|
await evaluate(
|
|
client,
|
|
`(() => {
|
|
const editor = document.querySelector('[data-testid="agent-gui-composer-editor"]');
|
|
if (!(editor instanceof HTMLElement)) throw new Error('composer editor is unavailable');
|
|
editor.focus();
|
|
document.execCommand('selectAll', false);
|
|
if (!document.execCommand('insertText', false, ${JSON.stringify(prompt)})) {
|
|
throw new Error('could not enter composer prompt');
|
|
}
|
|
return true;
|
|
})()`
|
|
);
|
|
await waitForEvaluation(
|
|
client,
|
|
`(() => {
|
|
const submit = document.querySelector('[data-testid="agent-gui-composer-send"]');
|
|
return { ready: submit instanceof HTMLButtonElement && !submit.disabled };
|
|
})()`,
|
|
timeoutMs,
|
|
"enabled composer submit button",
|
|
25
|
|
);
|
|
await evaluate(
|
|
client,
|
|
`(() => {
|
|
const submit = document.querySelector('[data-testid="agent-gui-composer-send"]');
|
|
if (!(submit instanceof HTMLButtonElement) || submit.disabled) throw new Error('composer send button is unavailable');
|
|
submit.click();
|
|
return true;
|
|
})()`
|
|
);
|
|
}
|