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.
This commit is contained in:
@@ -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 <role>: <model> (cycle: …)` line.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -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<HistoryStorage>
|
||||
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[] = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user