fix: align sidebar PR badges and expose run scope (#3020)
This commit is contained in:
@@ -421,31 +421,26 @@ func TestClient_FirstBootMissesGracefully(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cachePath := filepath.Join(dir, "models-dev.json")
|
||||
|
||||
// A cold-boot lookup schedules a background refresh against the
|
||||
// configured URL. Block that refresh at the server so it can't
|
||||
// populate the cache before the lookup reads it — otherwise the
|
||||
// "miss" we're asserting races a fast background fetch and flakes
|
||||
// into a hit. The request exits when the lookup context is canceled,
|
||||
// before TempDir cleanup can race a cache write.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
srv.Close()
|
||||
})
|
||||
// A cold-boot lookup schedules a detached background refresh. Hold that
|
||||
// request at the gate so it cannot populate the cache before the lookup
|
||||
// reads it, then release and join the refresh before TempDir cleanup.
|
||||
gate := newRequestGate(t, sampleDataset)
|
||||
c := modelsdev.New(modelsdev.Config{
|
||||
CachePath: cachePath,
|
||||
URL: srv.URL,
|
||||
URL: gate.server.URL,
|
||||
TTL: time.Hour,
|
||||
HTTPClient: srv.Client(),
|
||||
HTTPClient: gate.server.Client(),
|
||||
}, logger.Default())
|
||||
|
||||
// No Refresh — simulating cold boot before any HTTP fetch.
|
||||
if _, ok := c.LookupForModel(ctx, "claude-opus-4-7"); ok {
|
||||
if _, ok := c.LookupForModel(context.Background(), "claude-opus-4-7"); ok {
|
||||
t.Error("expected miss on cold-boot lookup")
|
||||
}
|
||||
gate.waitForFirstRequest(t)
|
||||
gate.releaseAll()
|
||||
if err := c.Refresh(context.Background()); err != nil {
|
||||
t.Fatalf("join background refresh: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type requestGate struct {
|
||||
|
||||
@@ -306,6 +306,10 @@ type RunDetailResponse struct {
|
||||
// context the run dispatcher built at claim time. Surfaced for
|
||||
// debugging.
|
||||
ContextSnapshot string `json:"context_snapshot,omitempty"`
|
||||
// ContinuationScope is the persisted summary scope selected when
|
||||
// the run was created. It lets run inspection show which
|
||||
// continuation-summary chain the run reads and updates.
|
||||
ContinuationScope string `json:"continuation_scope,omitempty"`
|
||||
// OutputSummary mirrors runs.output_summary — the free-form
|
||||
// agent output captured at run finish. Kept alongside ResultJSON
|
||||
// because legacy adapters populate this and not result_json.
|
||||
|
||||
@@ -276,16 +276,17 @@ func GetRunDetail(
|
||||
CachedTokens: costs.CachedTokens,
|
||||
CostSubcents: costs.CostSubcents,
|
||||
},
|
||||
Session: sessionDTO,
|
||||
Invocation: invocation,
|
||||
Runtime: runtimeDTO,
|
||||
TasksTouched: taskIDs,
|
||||
Events: eventDTOs,
|
||||
AssembledPrompt: run.AssembledPrompt,
|
||||
SummaryInjected: run.SummaryInjected,
|
||||
ResultJSON: run.ResultJSON,
|
||||
ContextSnapshot: run.ContextSnapshot,
|
||||
OutputSummary: run.OutputSummary,
|
||||
Session: sessionDTO,
|
||||
Invocation: invocation,
|
||||
Runtime: runtimeDTO,
|
||||
TasksTouched: taskIDs,
|
||||
Events: eventDTOs,
|
||||
AssembledPrompt: run.AssembledPrompt,
|
||||
SummaryInjected: run.SummaryInjected,
|
||||
ResultJSON: run.ResultJSON,
|
||||
ContextSnapshot: run.ContextSnapshot,
|
||||
ContinuationScope: run.ContinuationScope,
|
||||
OutputSummary: run.OutputSummary,
|
||||
}
|
||||
if run.CancelReason != nil && *run.CancelReason != "" {
|
||||
v := *run.CancelReason
|
||||
|
||||
@@ -2,6 +2,7 @@ package dashboard_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -395,4 +396,16 @@ func TestGetRunDetail_PromptArtifactsAndSnapshots(t *testing.T) {
|
||||
if resp.ContextSnapshot == "" {
|
||||
t.Errorf("expected context_snapshot to round-trip from default '{}', got empty string")
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal run detail response: %v", err)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
t.Fatalf("decode run detail response: %v", err)
|
||||
}
|
||||
if got := payload["continuation_scope"]; got != "agent:agent-1" {
|
||||
t.Errorf("continuation_scope = %v, want %q", got, "agent:agent-1")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ function TaskStateIcon({
|
||||
}
|
||||
|
||||
function TaskItemTitle({ title }: { title: string }) {
|
||||
return <ScrollOnOverflow className="min-w-0 w-full">{title}</ScrollOnOverflow>;
|
||||
return <ScrollOnOverflow className="min-w-0">{title}</ScrollOnOverflow>;
|
||||
}
|
||||
|
||||
function TaskItemContent({
|
||||
|
||||
@@ -181,5 +181,26 @@ test.describe("GitLab MR badge on the sidebar", () => {
|
||||
: "mr-then-pr";
|
||||
});
|
||||
expect(order).toBe("pr-then-mr");
|
||||
|
||||
const spacing = await row.evaluate((el, titleText) => {
|
||||
const title = [...el.querySelectorAll("span")].find(
|
||||
(candidate) =>
|
||||
candidate.classList.contains("whitespace-nowrap") && candidate.textContent === titleText,
|
||||
);
|
||||
const pr = el.querySelector('[data-testid^="pr-task-icon-"]');
|
||||
if (!title || !pr) return { found: false, gap: -1, titleTop: -1, prTop: -1 };
|
||||
const titleBox = title.getBoundingClientRect();
|
||||
const prBox = pr.getBoundingClientRect();
|
||||
return {
|
||||
found: true,
|
||||
gap: prBox.left - titleBox.right,
|
||||
titleTop: titleBox.top,
|
||||
prTop: prBox.top,
|
||||
};
|
||||
}, "Sidebar PR and MR badge task");
|
||||
expect(spacing.found).toBe(true);
|
||||
expect(Math.abs(spacing.titleTop - spacing.prTop)).toBeLessThan(4);
|
||||
expect(spacing.gap).toBeGreaterThanOrEqual(0);
|
||||
expect(spacing.gap).toBeLessThan(32);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { activeSessionId, seedSecondaryClarificationTask } from "../../helpers/clarification";
|
||||
import { makeGitEnv } from "../../helpers/git-helper";
|
||||
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
test.describe("Mobile sidebar task actions", () => {
|
||||
@@ -51,6 +52,66 @@ test.describe("Mobile sidebar task actions", () => {
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("keeps a linked PR badge beside its title in the phone drawer", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
const title = "Mobile sidebar PR badge spacing";
|
||||
const task = await apiClient.createTask(seedData.workspaceId, title, {
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
});
|
||||
await apiClient.mockGitHubAssociateTaskPR({
|
||||
task_id: task.id,
|
||||
owner: "testorg",
|
||||
repo: "testrepo",
|
||||
pr_number: 902,
|
||||
pr_url: "https://github.com/testorg/testrepo/pull/902",
|
||||
pr_title: "Mobile sidebar spacing",
|
||||
head_branch: "feature/mobile-sidebar-spacing",
|
||||
base_branch: "main",
|
||||
author_login: "e2e",
|
||||
state: "open",
|
||||
checks_state: "success",
|
||||
mergeable_state: "clean",
|
||||
});
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await session.mobileSessionMenu.tap();
|
||||
|
||||
const drawer = testPage.getByRole("dialog", { name: "Tasks" });
|
||||
const row = drawer.getByTestId("sidebar-task-item").filter({ hasText: title });
|
||||
await expect(row).toBeVisible();
|
||||
const prIcon = row.getByTestId(`pr-task-icon-${task.id}`);
|
||||
await expect(prIcon).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const spacing = await row.evaluate((el, titleText) => {
|
||||
const titleElement = [...el.querySelectorAll("span")].find(
|
||||
(candidate) =>
|
||||
candidate.classList.contains("whitespace-nowrap") && candidate.textContent === titleText,
|
||||
);
|
||||
const pr = el.querySelector('[data-testid^="pr-task-icon-"]');
|
||||
if (!titleElement || !pr) return { found: false, gap: -1, titleTop: -1, prTop: -1 };
|
||||
const titleBox = titleElement.getBoundingClientRect();
|
||||
const prBox = pr.getBoundingClientRect();
|
||||
return {
|
||||
found: true,
|
||||
gap: prBox.left - titleBox.right,
|
||||
titleTop: titleBox.top,
|
||||
prTop: prBox.top,
|
||||
};
|
||||
}, title);
|
||||
expect(spacing.found).toBe(true);
|
||||
expect(Math.abs(spacing.titleTop - spacing.prTop)).toBeLessThan(4);
|
||||
expect(spacing.gap).toBeGreaterThanOrEqual(0);
|
||||
expect(spacing.gap).toBeLessThan(32);
|
||||
await assertNoDocumentHorizontalOverflow(testPage, "mobile sidebar PR badge spacing");
|
||||
});
|
||||
|
||||
test("switches to the selected task and its chat from the phone task drawer", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -111,6 +111,8 @@ export type RunDetail = {
|
||||
summary_injected?: string;
|
||||
result_json?: string;
|
||||
context_snapshot?: string;
|
||||
// Persisted when the run is created. Identifies the continuation-summary chain.
|
||||
continuation_scope?: string;
|
||||
output_summary?: string;
|
||||
// Routing snapshot — omitted for legacy concrete-profile runs.
|
||||
routing?: RunRouting;
|
||||
|
||||
Reference in New Issue
Block a user