302 lines
12 KiB
JavaScript
302 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
|
|
test("renders every Pi RPC extension UI method without replying to fire-and-forget requests", async () => {
|
|
const source = await readFile("ui/src/App.tsx", "utf8");
|
|
for (const method of [
|
|
"select",
|
|
"confirm",
|
|
"input",
|
|
"editor",
|
|
"notify",
|
|
"setStatus",
|
|
"setWidget",
|
|
"setTitle",
|
|
"set_editor_text",
|
|
]) {
|
|
assert.match(source, new RegExp(`case "${method}"`));
|
|
}
|
|
assert.match(source, /setPendingExtension\(extension\)/);
|
|
assert.doesNotMatch(source, /This extension UI is unsupported/);
|
|
assert.match(source, /function stripAnsi/);
|
|
assert.match(source, /stripAnsi\(extension\.statusText\)/);
|
|
assert.match(source, /className="pi-controls"/);
|
|
assert.match(source, /aria-label="Model and thinking controls"/);
|
|
assert.doesNotMatch(source, /<details className="pi-controls">/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/event\.ctrlKey && event\.key\.toLowerCase\(\) === "c"/,
|
|
);
|
|
assert.match(source, /getCurrentWindow\(\)\s*\.hide\(\)\s*\.catch/);
|
|
assert.match(
|
|
source,
|
|
/window\.addEventListener\("keydown", handleKeydown, true\)/,
|
|
);
|
|
assert.match(source, /useLayoutEffect/);
|
|
assert.match(source, /transcript\.scrollTop = transcript\.scrollHeight/);
|
|
assert.match(source, /requestAnimationFrame\(scrollToLatest\)/);
|
|
assert.match(source, /const loadGeneration = useRef\(0\)/);
|
|
assert.match(
|
|
source,
|
|
/const bridgeRetryTimer = useRef<number \| undefined>\(undefined\)/,
|
|
);
|
|
assert.match(source, /const bridgeRetryAttempt = useRef\(0\)/);
|
|
assert.match(source, /function scheduleBridgeReconnect/);
|
|
assert.match(source, /Bridge unavailable; retrying in/);
|
|
assert.match(
|
|
source,
|
|
/setStatus\(String\(error\)\);\s*scheduleBridgeReconnect\(\);/,
|
|
);
|
|
assert.match(source, /bridgeRetryTimer\.current = window\.setTimeout/);
|
|
assert.match(source, /void refreshAgents\(preferredPath\)/);
|
|
assert.match(source, /bridgeRetryAttempt\.current = 0/);
|
|
assert.match(
|
|
source,
|
|
/const lastEventSequence = useRef<Record<string, number>>/,
|
|
);
|
|
assert.match(source, /generation !== loadGeneration\.current/);
|
|
assert.match(source, /current\.phase !== "working"/);
|
|
assert.match(source, /bridgeEvent\.seq <= \(lastEventSequence\.current/);
|
|
assert.match(source, /\.toFixed\(1\)/);
|
|
assert.match(source, /formatTokens\(stats\.tokens\.input\)/);
|
|
assert.match(source, /formatTokens\(stats\.tokens\.output\)/);
|
|
assert.match(source, /formatTokens\(cacheTokens\)/);
|
|
assert.match(source, /function updateWorkProgress/);
|
|
assert.match(
|
|
source,
|
|
/className=\{`work-progress composer-progress \$\{workProgress\.phase\}`\}/,
|
|
);
|
|
assert.match(source, /event\.type === "tool"/);
|
|
assert.match(source, /event\.type === "queue"/);
|
|
assert.match(source, /composer-progress/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/<section className=\{`work-progress \$\{workProgress\.phase\}`\}/,
|
|
);
|
|
});
|
|
|
|
test("exposes accessible directory, session, notification, and extension controls", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(component, /aria-expanded=\{view === "settings"\}/);
|
|
assert.match(
|
|
component,
|
|
/aria-label="Managed directories"\s+className="directory-tab-list"\s+role="tablist"/,
|
|
);
|
|
assert.match(component, /role="tab"/);
|
|
assert.match(
|
|
component,
|
|
/aria-selected=\{directory\.agentId === selectedId\}/,
|
|
);
|
|
assert.match(component, /aria-controls="directory-workspace"/);
|
|
assert.match(component, /onKeyDown=\{\(event\) => handleDirectoryTabKeyDown/);
|
|
assert.match(
|
|
component,
|
|
/aria-current=\{session\.isCurrent \? "page" : undefined\}/,
|
|
);
|
|
assert.match(component, /role="status" aria-atomic="true"/);
|
|
assert.match(component, /aria-hidden="true"/);
|
|
assert.match(
|
|
component,
|
|
/role=\{notice\.notifyType === "error" \? "alert" : "status"\}/,
|
|
);
|
|
assert.match(component, /role="dialog"/);
|
|
assert.match(component, /aria-modal="true"/);
|
|
assert.match(component, /aria-labelledby="extension-title"/);
|
|
assert.match(component, /const extensionRef = useRef<HTMLElement>\(null\)/);
|
|
assert.match(component, /function focusableExtensionControls/);
|
|
assert.match(component, /event\.key !== "Tab"/);
|
|
assert.match(component, /data-initial-focus/);
|
|
assert.match(component, /className="extension-backdrop"/);
|
|
assert.match(component, /htmlFor="extension-value"/);
|
|
assert.match(component, /Close current session and start a new session/);
|
|
assert.match(component, /Restart Pi for the selected directory/);
|
|
assert.match(component, /window\.confirm\(/);
|
|
assert.match(styles, /button:focus-visible/);
|
|
assert.match(styles, /\*::before,\n\*::after/);
|
|
assert.match(styles, /\.sr-only/);
|
|
assert.match(styles, /\.extension-backdrop \{/);
|
|
assert.match(styles, /animation-duration: 0\.01ms/);
|
|
});
|
|
|
|
test("makes the window draggable from the non-interactive header", async () => {
|
|
const [component, styles, capability] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
readFile("ui/src-tauri/capabilities/default.json", "utf8"),
|
|
]);
|
|
assert.match(component, /startDragging\(\)/);
|
|
assert.match(
|
|
component,
|
|
/closest\(\s*"button, input, select, textarea",?\s*\)/,
|
|
);
|
|
assert.match(styles, /\.workflow-header \{[\s\S]*cursor: grab/);
|
|
assert.match(capability, /core:window:allow-start-dragging/);
|
|
});
|
|
|
|
test("opens local follow-up choices for /model and /resume", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(component, /const controlCommands: Command\[\]/);
|
|
assert.match(component, /followUp: "model"/);
|
|
assert.match(component, /followUp: "resume"/);
|
|
assert.match(component, /followUp: "thinking"/);
|
|
assert.match(component, /const nativeCommand = controlCommands\.find/);
|
|
assert.match(component, /function chooseThinking/);
|
|
assert.match(component, /function openCommandFollowUp/);
|
|
assert.match(component, /function chooseModel/);
|
|
assert.match(component, /className="command-followup"/);
|
|
assert.match(component, /New session/);
|
|
assert.match(styles, /\.command-followup \{/);
|
|
assert.match(styles, /\.command-choice-list \{/);
|
|
});
|
|
|
|
test("shows bridge-managed directory status and sessions for the opened directory", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(
|
|
component,
|
|
/invoke<\{ directories: Directory\[\] \}>\("list_directories"\)/,
|
|
);
|
|
assert.match(
|
|
component,
|
|
/invoke<\{ sessions: Session\[\] \}>\("list_sessions"/,
|
|
);
|
|
assert.match(component, /function chooseDirectory/);
|
|
assert.match(component, /function handleDirectoryTabKeyDown/);
|
|
assert.match(component, /function forgetDirectory/);
|
|
assert.match(component, /window\.confirm/);
|
|
assert.match(component, /"forget_directory"/);
|
|
assert.match(component, /bridgeEvent\.type === "agent_state"/);
|
|
assert.match(component, /setDirectories\(\(current\) =>/);
|
|
assert.match(
|
|
component,
|
|
/state: loadedState\.isStreaming \? "streaming" : "idle"/,
|
|
);
|
|
assert.match(component, /className="directory-add"/);
|
|
assert.match(component, /Add directory/);
|
|
assert.doesNotMatch(component, /className="folder-form"/);
|
|
assert.doesNotMatch(component, /Remembered folders/);
|
|
assert.match(component, /function startNewSession/);
|
|
assert.match(component, /function closeCurrentSession/);
|
|
assert.match(component, /Use \/resume to reopen it/);
|
|
assert.match(component, /function switchSession/);
|
|
assert.match(component, /className="session-panel"/);
|
|
assert.match(component, /New session/);
|
|
assert.match(component, /state\.isStreaming/);
|
|
assert.match(styles, /\.directory-add \{/);
|
|
assert.match(styles, /\.directory-tab \{/);
|
|
assert.match(styles, /\.directory-tab-button \{/);
|
|
assert.match(styles, /\.directory-close \{/);
|
|
assert.match(styles, /\.session-panel \{/);
|
|
assert.match(styles, /\.session-list \{/);
|
|
});
|
|
|
|
test("renders todo-plugin state and compact context/token status", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(component, /function currentTodos/);
|
|
assert.match(component, /message\.toolName === "todo"/);
|
|
assert.match(component, /className="todos-pane"/);
|
|
assert.match(component, /className="workflow-main"/);
|
|
assert.match(component, /ctx \{formatTokens\(contextTokens\)\}/);
|
|
assert.match(component, /tok \{formatTokens\(stats\.tokens\.total\)\}/);
|
|
assert.match(styles, /\.workflow-main \{/);
|
|
assert.match(styles, /\.todos-pane \{/);
|
|
assert.match(styles, /\.status-metric \{/);
|
|
});
|
|
|
|
test("shows an optimistic prompt until the transcript receives it", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(component, /type PendingSubmission/);
|
|
assert.match(
|
|
component,
|
|
/const \[pendingSubmissions, setPendingSubmissions\]/,
|
|
);
|
|
assert.match(component, /setStatus\("Sending prompt to Pi…"\)/);
|
|
assert.match(component, /phase: "sending"/);
|
|
assert.match(component, /phase: "sent"/);
|
|
assert.match(component, /Sent · waiting for Pi/);
|
|
assert.match(component, /receivedMessages\.some/);
|
|
assert.match(component, /pending-message/);
|
|
assert.match(styles, /\.pending-message \{/);
|
|
assert.match(styles, /\.pending-message\.sending/);
|
|
});
|
|
|
|
test("distinguishes user and assistant messages", async () => {
|
|
const styles = await readFile("ui/src/App.css", "utf8");
|
|
assert.match(styles, /\.message\.user \{/);
|
|
assert.match(styles, /background: #192536/);
|
|
assert.match(styles, /\.message\.assistant \{/);
|
|
assert.match(styles, /background: #1a241a/);
|
|
});
|
|
|
|
test("keeps an accessible focus treatment for the prompt editor", async () => {
|
|
const styles = await readFile("ui/src/App.css", "utf8");
|
|
assert.match(styles, /\.composer textarea:focus \{/);
|
|
assert.doesNotMatch(styles, /outline: none/);
|
|
assert.match(styles, /border-color: #6f8b56/);
|
|
assert.match(
|
|
styles,
|
|
/\.composer textarea:focus \{[^}]*outline: 2px solid #c4ed8b/,
|
|
);
|
|
assert.match(styles, /\.composer textarea:focus \{[^}]*outline-offset: -2px/);
|
|
assert.doesNotMatch(styles, /\.composer textarea:focus \{[^}]*box-shadow:/);
|
|
assert.match(styles, /textarea:focus-visible/);
|
|
});
|
|
|
|
test("submits the composer with Enter and preserves Shift+Enter for newlines", async () => {
|
|
const source = await readFile("ui/src/App.tsx", "utf8");
|
|
assert.match(source, /onKeyDown=\{\(event\) => \{/);
|
|
assert.match(source, /event\.key !== "Enter"/);
|
|
assert.match(source, /event\.shiftKey/);
|
|
assert.match(source, /event\.nativeEvent\.isComposing/);
|
|
assert.match(source, /event\.preventDefault\(\);\s*void submit\(\);/);
|
|
assert.match(source, /Enter to send · Shift\+Enter for newline/);
|
|
});
|
|
|
|
test("uses a compact workflow layout that keeps the composer and transcript stable", async () => {
|
|
const [component, styles] = await Promise.all([
|
|
readFile("ui/src/App.tsx", "utf8"),
|
|
readFile("ui/src/App.css", "utf8"),
|
|
]);
|
|
assert.match(component, /className="workflow-header"/);
|
|
assert.match(component, /className="workflow"/);
|
|
assert.match(component, /className="workflow-footer"/);
|
|
assert.match(component, /className="composer-stack"/);
|
|
assert.match(
|
|
styles,
|
|
/\.workflow \{[\s\S]*grid-template-rows: minmax\(0, 1fr\) auto/,
|
|
);
|
|
assert.match(styles, /\.transcript,\s*\.settings \{[\s\S]*overflow: auto/);
|
|
assert.match(
|
|
component,
|
|
/className=\{`transcript \$\{pendingExtension \? "with-extension" : ""\} \$\{workProgress\.phase\}`\}/,
|
|
);
|
|
assert.match(
|
|
styles,
|
|
/\.transcript\.with-extension \{[\s\S]*padding-bottom: 112px/,
|
|
);
|
|
assert.match(styles, /\.transcript\.working \{/);
|
|
assert.match(styles, /conic-gradient\(/);
|
|
assert.match(styles, /transcript-border-orbit/);
|
|
assert.match(styles, /prefers-reduced-motion: reduce/);
|
|
assert.match(
|
|
styles,
|
|
/\.workflow-main \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) 210px/,
|
|
);
|
|
assert.match(styles, /\.pi-controls \{[\s\S]*padding: 4px 6px/);
|
|
});
|