From edcc8ab30f0f2cdbcb253b1d59c810c8ac3d791d Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 31 May 2026 01:04:56 +0200 Subject: [PATCH] test(coding-agent): added segment-track coverage and removed obsolete OAuth selector spec - Added unit tests for renderSegmentTrack verifying per-segment coloring, active chip fill formatting, and active-index movement. - Added tests for theme.getContrastFgAnsi to ensure selected foreground contrast remains clearly readable against RGB fills. - Removed the OAuth selector keybinding test and its local auth-storage scaffolding, and documented the new model-tier/role-cycle chip-track behavior in the changelog. --- packages/coding-agent/CHANGELOG.md | 1 + .../keybindings-selector-navigation.test.ts | 61 ---------------- .../modes/components/segment-track.test.ts | 72 +++++++++++++++++++ 3 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 packages/coding-agent/test/modes/components/segment-track.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d19e7a0cd..bf30bd5b7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ - Changed setup onboarding to a tabbed `Set up your providers` scene with dedicated `Sign in` and `Web search` panels - Changed the glyph mode picker to preselect the currently configured symbol preset instead of always defaulting to Unicode and to show live glyph samples in the picker rows - Changed OAuth sign-in flow in the setup wizard so users can authenticate multiple providers before leaving with Escape +- Changed the plan-approval model-tier slider and the `ctrl+p`/`alt+p` role-cycle status to share one status-line-style chip track: each tier renders in its own role color and the active tier is filled as a powerline chip with a luminance-matched label. The role-cycle status now shows only the chip track — the resolved model and thinking level already live on the status line — instead of the verbose `Switched to : (cycle: …)` line. ### Fixed diff --git a/packages/coding-agent/test/keybindings-selector-navigation.test.ts b/packages/coding-agent/test/keybindings-selector-navigation.test.ts index 3877f51ce..199979220 100644 --- a/packages/coding-agent/test/keybindings-selector-navigation.test.ts +++ b/packages/coding-agent/test/keybindings-selector-navigation.test.ts @@ -7,17 +7,10 @@ import { KeybindingsManager } from "@oh-my-pi/pi-coding-agent/config/keybindings import { ExtensionList } from "@oh-my-pi/pi-coding-agent/modes/components/extensions/extension-list"; import type { Extension } from "@oh-my-pi/pi-coding-agent/modes/components/extensions/types"; import { HistorySearchComponent } from "@oh-my-pi/pi-coding-agent/modes/components/history-search"; -import { OAuthSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/oauth-selector"; import { SessionSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/session-selector"; import { TreeSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/tree-selector"; import { UserMessageSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/user-message-selector"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; -import { - type AuthCredential, - type AuthCredentialStore, - AuthStorage, - type StoredAuthCredential, -} from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { HistoryStorage } from "@oh-my-pi/pi-coding-agent/session/history-storage"; import type { SessionInfo, SessionTreeNode } from "@oh-my-pi/pi-coding-agent/session/session-manager"; import { setKeybindings } from "@oh-my-pi/pi-tui"; @@ -31,40 +24,6 @@ const TEST_KEYBINDINGS = KeybindingsManager.inMemory({ const tempDirs: string[] = []; -class EmptyCredentialStore implements AuthCredentialStore { - close(): void {} - - listAuthCredentials(_provider?: string): StoredAuthCredential[] { - return []; - } - - updateAuthCredential(_id: number, _credential: AuthCredential): void {} - - deleteAuthCredential(_id: number, _disabledCause: string): void {} - - tryDisableAuthCredentialIfMatches(_id: number, _expectedData: string, _disabledCause: string): boolean { - return false; - } - - replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[] { - return []; - } - - upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[] { - return []; - } - - deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void {} - - getCache(_key: string, _options?: { includeExpired?: boolean }): string | null { - return null; - } - - setCache(_key: string, _value: string, _expiresAtSec: number): void {} - - cleanExpiredCache(): void {} -} - beforeAll(() => { initTheme(); }); @@ -133,10 +92,6 @@ async function createHistoryStorage(prompts: string[]): Promise return storage; } -function createAuthStorage(): AuthStorage { - return new AuthStorage(new EmptyCredentialStore()); -} - describe("selector navigation keybindings", () => { it("uses tui.select.down in the session selector", () => { setKeybindings(TEST_KEYBINDINGS); @@ -201,22 +156,6 @@ describe("selector navigation keybindings", () => { expect(list.getSelectedExtension()?.id).toBe("tool-a"); }); - it("uses tui.select.down in the OAuth selector", () => { - setKeybindings(TEST_KEYBINDINGS); - const selected: string[] = []; - const selector = new OAuthSelectorComponent( - "login", - createAuthStorage(), - id => selected.push(id), - () => {}, - ); - - selector.handleInput(CTRL_N); - selector.handleInput("\n"); - - expect(selected).toEqual(["alibaba-coding-plan"]); - }); - it("uses tui.select.down in history search", async () => { setKeybindings(TEST_KEYBINDINGS); const selected: string[] = []; diff --git a/packages/coding-agent/test/modes/components/segment-track.test.ts b/packages/coding-agent/test/modes/components/segment-track.test.ts new file mode 100644 index 000000000..ca93b1e30 --- /dev/null +++ b/packages/coding-agent/test/modes/components/segment-track.test.ts @@ -0,0 +1,72 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { renderSegmentTrack, type TrackSegment } from "@oh-my-pi/pi-coding-agent/modes/components/segment-track"; +import { initTheme, type ThemeColor, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; + +beforeAll(async () => { + await initTheme(); +}); + +const SEGMENTS: TrackSegment[] = [ + { label: "smol", color: "warning" }, + { label: "default", color: "success" }, + { label: "slow", color: "accent" }, +]; + +/** Pull the RGB out of a truecolor fg/bg escape, or null for 256-palette ones. */ +function rgb(ansi: string): [number, number, number] | null { + const m = /3[84];2;(\d+);(\d+);(\d+)/.exec(ansi); + return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null; +} + +function luma([r, g, b]: [number, number, number]): number { + return 0.299 * r + 0.587 * g + 0.114 * b; +} + +describe("renderSegmentTrack", () => { + it("renders every segment in its own color", () => { + const raw = renderSegmentTrack(SEGMENTS, 1); + expect(Bun.stripANSI(raw)).toContain("smol"); + expect(Bun.stripANSI(raw)).toContain("default"); + expect(Bun.stripANSI(raw)).toContain("slow"); + // Each segment carries the foreground escape for its assigned theme color. + for (const seg of SEGMENTS) { + expect(raw).toContain(theme.getFgAnsi(seg.color as ThemeColor)); + } + }); + + it("fills exactly the active segment as a bold chip with a background", () => { + const raw = renderSegmentTrack(SEGMENTS, 1); + // One filled chip: a single bold run and a single background fill. + expect(raw.match(/\x1b\[1m/g)?.length).toBe(1); + expect(raw.match(/48;2;/g)?.length).toBe(1); + // The active label sits inside the bold run, and the fill is its own accent. + expect(raw).toContain("\x1b[1m default \x1b[22m"); + const activeBg = theme.getFgAnsi("success").replace("\x1b[38;", "\x1b[48;"); + expect(raw).toContain(activeBg); + }); + + it("moves the filled chip with the active index", () => { + expect(renderSegmentTrack(SEGMENTS, 0)).toContain("\x1b[1m smol \x1b[22m"); + expect(renderSegmentTrack(SEGMENTS, 2)).toContain("\x1b[1m slow \x1b[22m"); + // A non-active label is never wrapped in the bold chip run. + expect(renderSegmentTrack(SEGMENTS, 0)).not.toContain("\x1b[1m slow \x1b[22m"); + }); +}); + +describe("theme.getContrastFgAnsi", () => { + it("returns a high-contrast near-black/near-white over any fill", () => { + const BLACK = "\x1b[38;2;0;0;0m"; + const WHITE = "\x1b[38;2;255;255;255m"; + const colors: ThemeColor[] = ["warning", "accent", "success", "error", "border", "muted", "text"]; + for (const color of colors) { + const fill = rgb(theme.getFgAnsi(color)); + if (!fill) continue; // 256-palette terminal: falls back to `text`, not under test here + const picked = theme.getContrastFgAnsi(color); + expect(picked === BLACK || picked === WHITE).toBe(true); + const pickedRgb = rgb(picked); + expect(pickedRgb).not.toBeNull(); + // Whichever it picked must read clearly against the fill. + expect(Math.abs(luma(pickedRgb as [number, number, number]) - luma(fill))).toBeGreaterThan(100); + } + }); +});