Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions tests/ConversationLayoutTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ struct ConversationWidgetTestAccess
ConversationWidget& conversation, QWidget* anchor, int anchorY)
{
auto* scroll = conversation.scrollArea->verticalScrollBar();
scroll->setValue(scroll->minimum());
conversation.followingLatest = false;
conversation.pinLatestDuringLayout = true;
conversation.layoutSettleTimer->start(60'000);
Expand Down Expand Up @@ -80,6 +79,14 @@ struct ConversationWidgetTestAccess
conversation.pendingTimelineShrink = false;
conversation.scrollArea->viewport()->setUpdatesEnabled(true);
}

static void settleAnchoredReconciliation(ConversationWidget& conversation)
{
conversation.layoutSettleTimer->stop();
conversation.layoutSettleTimer->setInterval(16);
conversation.pinLatestDuringLayout = false;
conversation.settleTimelineLayout();
}
};

} // namespace codexui
Expand Down Expand Up @@ -1056,13 +1063,18 @@ bool testPointerPreservingAppend()
beforeFixture.turns.front().messages.push_back(
{"item-append-257", frontend::ThreadItemKind::AgentMessage, "final answer"});
const client::State after = makeState({beforeFixture});
ThreadFixture latestFixture = beforeFixture;
latestFixture.turns.front().messages.back().text = "updated final answer";
const client::State latest = makeState({latestFixture});
int latestPresentationRequests = 0;
QObject::connect(&conversation,
&codexui::ConversationWidget::latestPresentationRequested,
&conversation,
[&latestPresentationRequests] { ++latestPresentationRequests; });
conversation.render(after, QStringLiteral("append"));
settleTimeline();
conversation.render(latest, QStringLiteral("append"));
settleTimeline();

QWidget* host = timeline(conversation);
const int frozenAnchorY = scroll && readingAnchor
Expand All @@ -1072,14 +1084,15 @@ bool testPointerPreservingAppend()
passed &= expect(readingHistory && evicted && survivor
&& survivor.data() == survivorAddress
&& qAbs(frozenAnchorY - anchorYBefore) <= 2
&& !segment(conversation, QStringLiteral("message:item-append-256")),
"an off-bottom reader must see a completely frozen presentation while canonical state advances");
&& !segment(conversation, QStringLiteral("message:item-append-256"))
&& !hasLabel(conversation, QStringLiteral("updated final answer")),
"an off-bottom reader must see a completely frozen presentation across repeated canonical updates");
if (scroll)
scroll->verticalScrollBar()->setValue(scroll->verticalScrollBar()->maximum());
settleEvents();
passed &= expect(latestPresentationRequests == 1,
"returning to the tail must request exactly one latest authoritative presentation");
conversation.render(after, QStringLiteral("append"));
conversation.render(latest, QStringLiteral("append"));
settleTimeline();

passed &= expect(!evicted && survivor && survivor.data() == survivorAddress
Expand All @@ -1088,7 +1101,7 @@ bool testPointerPreservingAppend()
passed &= expect(segment(conversation, QStringLiteral("message:item-append-256")) != nullptr
&& segment(conversation, QStringLiteral("message:item-append-257")) != nullptr
&& hasLabel(conversation, QStringLiteral("reflected prompt"))
&& hasLabel(conversation, QStringLiteral("final answer")),
&& hasLabel(conversation, QStringLiteral("updated final answer")),
"the reflected prompt and final answer must append at the timeline tail");
passed &= expect(host && host->property("renderedTimelineItems").toLongLong()
<= host->property("maximumRenderedItems").toLongLong(),
Expand All @@ -1099,13 +1112,24 @@ bool testPointerPreservingAppend()
followingConversation.show();
followingConversation.render(before, QStringLiteral("append"));
settleTimeline();
QPointer<QWidget> followedSurvivor = segment(
followingConversation, QStringLiteral("message:item-append-10"));
QWidget* const followedSurvivorAddress = followedSurvivor.data();
followingConversation.render(after, QStringLiteral("append"));
followingConversation.render(latest, QStringLiteral("append"));
settleTimeline();
QScrollArea* followingScroll = followingConversation.findChild<QScrollArea*>();
passed &= expect(followingScroll
&& followingScroll->verticalScrollBar()->value()
== followingScroll->verticalScrollBar()->maximum(),
"a followed append must settle smoothly at the newest timeline content");
== followingScroll->verticalScrollBar()->maximum()
&& followedSurvivor
&& followedSurvivor.data() == followedSurvivorAddress
&& followedSurvivor.data()
== segment(followingConversation,
QStringLiteral("message:item-append-10"))
&& hasLabel(followingConversation,
QStringLiteral("updated final answer")),
"rapid followed updates must retain widgets and settle at the newest timeline content");
return passed;
}

Expand Down Expand Up @@ -1236,7 +1260,19 @@ bool testKeyedSegmentInsertion()
&& activityLayout->indexOf(activity.data()) == 1,
"reordering unchanged segment keys must move the original QWidgets into the new order");

constexpr int anchoredY = 37;
QScrollArea* activityScroll = activityConversation.findChild<QScrollArea*>();
if (activityScroll && activity)
{
const int currentY = activityScroll->viewport()->mapFromGlobal(
activity->mapToGlobal(QPoint{})).y();
auto* bar = activityScroll->verticalScrollBar();
bar->setValue(qBound(0, bar->value() + currentY - 96, bar->maximum()));
settleEvents();
}
const int anchoredY = activityScroll && activity
? activityScroll->viewport()->mapFromGlobal(
activity->mapToGlobal(QPoint{})).y()
: 0;
const bool anchorPathAvailable =
codexui::ConversationWidgetTestAccess::primeViewportAnchor(
activityConversation, activity.data(), anchoredY);
Expand All @@ -1247,15 +1283,26 @@ bool testKeyedSegmentInsertion()
activityConversation.render(
makeState({activityFixture}),
QStringLiteral("keyed-activity-growth"));
passed &= expect(
const bool survivingAnchorTracked =
anchorPathAvailable
&& codexui::ConversationWidgetTestAccess::viewportAnchor(
activityConversation)
== activity.data()
&& codexui::ConversationWidgetTestAccess::viewportAnchorY(
activityConversation)
== anchoredY,
"reordering a surviving segment must preserve its pending viewport anchor and offset");
== anchoredY;
codexui::ConversationWidgetTestAccess::settleAnchoredReconciliation(
activityConversation);
const int settledAnchorY = activityScroll && activity
? activityScroll->viewport()->mapFromGlobal(
activity->mapToGlobal(QPoint{})).y()
: 0;
passed &= expect(
survivingAnchorTracked && activity
&& qAbs(settledAnchorY - anchoredY) <= 2
&& !codexui::ConversationWidgetTestAccess::viewportAnchor(
activityConversation),
"reordering a surviving segment must preserve its widget and observable viewport position");

codexui::ConversationWidgetTestAccess::primeViewportAnchor(
activityConversation, activity.data(), anchoredY);
Expand Down
22 changes: 19 additions & 3 deletions tests/Phase1ThreadTurnUxTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,12 @@ bool testUpcomingTurnCanonicalRebase()
auto* cwd = dock.findChild<QLineEdit*>(QStringLiteral("upcomingWorkspace"));
auto* settings = dock.findChild<QFrame*>(QStringLiteral("upcomingTurnSettings"));
auto* composer = dock.findChild<QFrame*>(QStringLiteral("upcomingComposer"));
auto* editor = dock.findChild<QPlainTextEdit*>(QStringLiteral("upcomingPromptEditor"));
auto* send = dock.findChild<QPushButton*>(QStringLiteral("upcomingSendButton"));
const auto stableControlTree = dock.findChildren<QWidget*>();
bool passed = expect(model && effort && cwd,
"the upcoming-turn canonical controls must be discoverable");
if (!model || !effort || !cwd || !settings || !composer)
if (!model || !effort || !cwd || !settings || !composer || !editor || !send)
return false;
const int stableBaseHeight = dock.height();
const int stableComposerTop = composer->geometry().top();
Expand Down Expand Up @@ -228,6 +231,13 @@ bool testUpcomingTurnCanonicalRebase()
&& cwd->text() == QStringLiteral("/workspace/refreshed")
&& effort->currentData().toString() == QStringLiteral("xhigh"),
"same-thread refreshes must rebase untouched controls without overwriting a user change");
passed &= expect(model == dock.findChild<QComboBox*>(QStringLiteral("upcomingModel"))
&& effort == dock.findChild<QComboBox*>(QStringLiteral("upcomingReasoning"))
&& cwd == dock.findChild<QLineEdit*>(QStringLiteral("upcomingWorkspace"))
&& editor == dock.findChild<QPlainTextEdit*>(QStringLiteral("upcomingPromptEditor"))
&& send == dock.findChild<QPushButton*>(QStringLiteral("upcomingSendButton"))
&& stableControlTree == dock.findChildren<QWidget*>(),
"a harmless same-thread refresh must retain the complete upcoming-turn control tree");
passed &= expect(dock.draft().effort.hasValue()
&& dock.draft().effort->value == "xhigh",
"a same-thread state update must preserve the pending typed override");
Expand Down Expand Up @@ -606,15 +616,17 @@ bool testUpcomingTurnActionStates()
"a running turn must permit steering and stopping while locking execution settings");
QString shortcutPrompt;
bool shortcutSteering = false;
int submissionCount = 0;
QObject::connect(&dock, &codexui::UpcomingTurnDock::sendRequested,
[&shortcutPrompt, &shortcutSteering](const QString& prompt, bool steering) {
[&shortcutPrompt, &shortcutSteering, &submissionCount](const QString& prompt, bool steering) {
++submissionCount;
shortcutPrompt = prompt;
shortcutSteering = steering;
});
QKeyEvent submitShortcut(QEvent::KeyPress, Qt::Key_Return, Qt::ControlModifier);
QCoreApplication::sendEvent(editor, &submitShortcut);
passed &= expect(shortcutPrompt == QStringLiteral("redirect the active turn")
&& shortcutSteering,
&& shortcutSteering && submissionCount == 1,
"the extracted prompt editor must preserve Ctrl+Enter submission semantics");
dock.setActionState(false,
false,
Expand All @@ -641,6 +653,10 @@ bool testUpcomingTurnActionStates()
&& !send->isEnabled() && !send->toolTip().isEmpty()
&& status && status->text().contains(QStringLiteral("previous active turn")),
"a steering draft must stay bound to its exact active turn and remain blocked after turn rollover");
QCoreApplication::sendEvent(editor, &submitShortcut);
send->click();
passed &= expect(submissionCount == 1,
"an obsolete turn-bound control state must not submit through click or Ctrl+Enter");
editor->insertPlainText(QStringLiteral(" "));
passed &= expect(send->isEnabled() && send->toolTip().isEmpty()
&& status && status->text() == QStringLiteral("Ctrl+Enter to steer"),
Expand Down