Merge remote-tracking branch 'origin/main' into pr-36-generic-agent-config

# Conflicts:
#	docs/config.html
#	docs/config.md
#	src/cli.test.ts
#	src/cli.ts
#	src/client/src/components/settings/SettingsSessiondPanel.ts
#	src/client/src/components/settings/settingsConfigDraft.test.ts
#	src/client/src/components/settings/settingsConfigDraft.ts
#	src/server/app.test.ts
#	src/server/app.ts
#	src/server/configRoutes.test.ts
#	src/server/configRoutes.ts
#	src/server/piWebPluginService.test.ts
#	src/server/piWebPluginService.ts
#	src/server/piWebStatus.test.ts
#	src/server/piWebStatus.ts
#	src/server/piWebStatusCache.ts
#	src/server/sessions/authService.test.ts
#	src/server/sessions/piSessionService.ts
#	src/server/sessions/sessionRoutes.test.ts
This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 20:30:08 +02:00
289 changed files with 29164 additions and 6419 deletions
+2 -9
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isSessionActive, sessionActivityLabel, isWorkspaceActivityActive } from "./activity";
import { isSessionActive, isWorkspaceActivityActive } from "./activity";
import type { SessionStatus, WorkspaceActivity } from "./apiTypes";
const idleStatus: SessionStatus = {
@@ -14,17 +14,10 @@ const idleStatus: SessionStatus = {
};
describe("activity helpers", () => {
it("detects and labels active session states consistently", () => {
it("detects active session states", () => {
expect(isSessionActive(idleStatus)).toBe(false);
expect(sessionActivityLabel(idleStatus)).toBeUndefined();
expect(isSessionActive({ ...idleStatus, isStreaming: true })).toBe(true);
expect(sessionActivityLabel({ ...idleStatus, isStreaming: true })).toBe("streaming");
expect(isSessionActive({ ...idleStatus, pendingMessageCount: 2 })).toBe(true);
expect(sessionActivityLabel({ ...idleStatus, pendingMessageCount: 2 })).toBe("2 pending");
expect(sessionActivityLabel(idleStatus, { sessionId: "s1", phase: "active", label: "running tool", detail: "read", at: "now" })).toBe("running tool: read");
});
it("detects workspace activity presence without exposing details", () => {
-10
View File
@@ -8,16 +8,6 @@ export function isSessionActive(status?: SessionStatus, activity?: SessionActivi
|| (status?.pendingMessageCount ?? 0) > 0;
}
export function sessionActivityLabel(status?: SessionStatus, activity?: SessionActivity): string | undefined {
if (activity?.phase === "active") return activity.detail !== undefined && activity.detail !== "" ? `${activity.label}: ${activity.detail}` : activity.label;
if (status === undefined) return undefined;
if (status.isCompacting) return "compacting";
if (status.isBashRunning) return "bash";
if (status.isStreaming) return "streaming";
if (status.pendingMessageCount > 0) return `${String(status.pendingMessageCount)} pending`;
return undefined;
}
export function isWorkspaceActivityActive(activity: WorkspaceActivity | undefined): boolean {
return activity !== undefined && (activity.hasSessionActivity || activity.hasTerminalActivity);
}
+76 -1
View File
@@ -3,10 +3,14 @@ export type MachineStatus = "unknown" | "online" | "offline" | "error";
export const PI_WEB_CAPABILITIES = {
sessionsDeleteArchived: "sessions.deleteArchived",
sessionsBulkMutations: "sessions.bulkMutations",
sessionsCleanup: "sessions.cleanup",
sessionsReload: "sessions.reload",
sessionsPersistedState: "sessions.persistedState",
promptAttachments: "prompt.attachments",
workspaceFileSuggestions: "workspace.fileSuggestions",
piPackagesManage: "piPackages.manage",
selectedMachineSettings: "settings.selectedMachine",
} as const;
export type PiWebCapability = typeof PI_WEB_CAPABILITIES[keyof typeof PI_WEB_CAPABILITIES];
@@ -108,6 +112,43 @@ export interface PiWebPluginsResponse {
plugins: PiWebPluginInfo[];
}
export type PiPackageScope = "user" | "project";
export interface PiPackageInfo {
source: string;
scope: PiPackageScope;
filtered: boolean;
installedPath?: string;
}
export interface PiPackagesResponse {
packages: PiPackageInfo[];
}
export interface PiPackageInstallRequest {
source: string;
}
export interface PiPackageRemoveRequest {
source: string;
/** Optional known scope from a listed package; not an install-location picker. */
scope?: PiPackageScope;
}
export interface PiPackageUpdateRequest {
/** Omit to update all configured Pi packages. */
source?: string;
}
export type PiPackageMutationAction = "install" | "remove" | "update";
export interface PiPackageMutationResponse extends PiPackagesResponse {
action: PiPackageMutationAction;
source?: string;
scope?: PiPackageScope;
removed?: boolean;
}
export interface PiWebConfigEnvOverrides {
host: boolean;
port: boolean;
@@ -158,6 +199,8 @@ export interface SessionRef {
export interface SessionInfo extends SessionRef {
path: string;
/** True when the server has verified a backing session file exists; false when known transient. */
persisted?: boolean;
name?: string;
created: string;
modified: string;
@@ -175,6 +218,34 @@ export interface ArchiveSessionsResponse {
skippedAlreadyArchivedCount?: number;
}
export interface SessionBulkMutationRef {
id: string;
cwd?: string;
}
export interface SessionBulkMutationRequest {
sessions: SessionBulkMutationRef[];
}
export interface SessionBulkFailure {
sessionId: string;
error: string;
}
export interface SessionBulkArchiveResponse {
archived: true;
archivedSessionIds: string[];
failures: SessionBulkFailure[];
generatedAt: string;
}
export interface SessionBulkDeleteArchivedResponse {
deleted: true;
deletedSessionIds: string[];
failures: SessionBulkFailure[];
generatedAt: string;
}
export interface SessionCleanupRequest {
/** Archive non-archived sessions whose modified time is older than this many days. Omit/null to disable. */
archiveIdleDays?: number | null;
@@ -324,6 +395,8 @@ export interface ThinkingLevelsResponse {
export interface SessionStatus {
sessionId: string;
/** True when the server has verified a backing session file exists; false when known transient. */
persisted?: boolean;
model?: SessionModel;
thinkingLevel?: string;
isStreaming: boolean;
@@ -497,7 +570,8 @@ export interface TerminalCommandRunFilter {
export type PiWebServiceComponent = "web" | "sessiond";
export type PiWebStatusSeverity = "info" | "warning" | "error";
export type PiWebInstallationKind = "pi-package" | "npm-global" | "local" | "unknown";
export type PiWebInstallationKind = "pi-package" | "npm-global" | "local" | "docker" | "unknown";
export type PiWebDockerMode = "runtime" | "dev";
export interface PiWebInstallationInfo {
kind: PiWebInstallationKind;
@@ -505,6 +579,7 @@ export interface PiWebInstallationInfo {
source?: string;
scope?: "user" | "project";
npmRoot?: string;
dockerMode?: PiWebDockerMode;
}
export interface PiWebComponentStatus {
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { effectivePiWebCapabilities, PI_WEB_CAPABILITIES, SESSIOND_RUNTIME_CAPABILITIES, WEB_RUNTIME_CAPABILITIES, parseKnownPiWebCapabilities } from "./capabilities";
describe("PI WEB capabilities", () => {
it("advertises web-only capabilities without requiring session daemon support", () => {
expect(WEB_RUNTIME_CAPABILITIES).toContain(PI_WEB_CAPABILITIES.piPackagesManage);
expect(WEB_RUNTIME_CAPABILITIES).toContain(PI_WEB_CAPABILITIES.selectedMachineSettings);
expect(SESSIOND_RUNTIME_CAPABILITIES).not.toContain(PI_WEB_CAPABILITIES.piPackagesManage);
expect(SESSIOND_RUNTIME_CAPABILITIES).not.toContain(PI_WEB_CAPABILITIES.selectedMachineSettings);
expect(effectivePiWebCapabilities({
web: { available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings] },
sessiond: { available: false, capabilities: [] },
})).toEqual([PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings]);
});
it("requires web and session daemon support for authoritative session persistence", () => {
expect(WEB_RUNTIME_CAPABILITIES).toContain(PI_WEB_CAPABILITIES.sessionsPersistedState);
expect(SESSIOND_RUNTIME_CAPABILITIES).toContain(PI_WEB_CAPABILITIES.sessionsPersistedState);
expect(effectivePiWebCapabilities({
web: { available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsPersistedState] },
sessiond: { available: false, capabilities: [PI_WEB_CAPABILITIES.sessionsPersistedState] },
})).not.toContain(PI_WEB_CAPABILITIES.sessionsPersistedState);
expect(effectivePiWebCapabilities({
web: { available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsPersistedState] },
sessiond: { available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsPersistedState] },
})).toContain(PI_WEB_CAPABILITIES.sessionsPersistedState);
});
it("keeps only known string capabilities when parsing runtime data", () => {
expect(parseKnownPiWebCapabilities([PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings, "future.capability"])).toEqual([PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings]);
expect(parseKnownPiWebCapabilities([PI_WEB_CAPABILITIES.piPackagesManage, 1])).toBeUndefined();
});
});
+29 -2
View File
@@ -6,15 +6,37 @@ export type { PiWebCapability };
export const KNOWN_PI_WEB_CAPABILITIES = Object.values(PI_WEB_CAPABILITIES);
const knownPiWebCapabilities: ReadonlySet<string> = new Set(KNOWN_PI_WEB_CAPABILITIES);
export const WEB_RUNTIME_CAPABILITIES = [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.sessionsCleanup, PI_WEB_CAPABILITIES.sessionsReload, PI_WEB_CAPABILITIES.promptAttachments, PI_WEB_CAPABILITIES.workspaceFileSuggestions] as const satisfies readonly PiWebCapability[];
export const SESSIOND_RUNTIME_CAPABILITIES = [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.sessionsCleanup, PI_WEB_CAPABILITIES.sessionsReload, PI_WEB_CAPABILITIES.promptAttachments] as const satisfies readonly PiWebCapability[];
export const WEB_RUNTIME_CAPABILITIES = [
PI_WEB_CAPABILITIES.sessionsDeleteArchived,
PI_WEB_CAPABILITIES.sessionsBulkMutations,
PI_WEB_CAPABILITIES.sessionsCleanup,
PI_WEB_CAPABILITIES.sessionsReload,
PI_WEB_CAPABILITIES.sessionsPersistedState,
PI_WEB_CAPABILITIES.promptAttachments,
PI_WEB_CAPABILITIES.workspaceFileSuggestions,
PI_WEB_CAPABILITIES.piPackagesManage,
PI_WEB_CAPABILITIES.selectedMachineSettings,
] as const satisfies readonly PiWebCapability[];
export const SESSIOND_RUNTIME_CAPABILITIES = [
PI_WEB_CAPABILITIES.sessionsDeleteArchived,
PI_WEB_CAPABILITIES.sessionsBulkMutations,
PI_WEB_CAPABILITIES.sessionsCleanup,
PI_WEB_CAPABILITIES.sessionsReload,
PI_WEB_CAPABILITIES.sessionsPersistedState,
PI_WEB_CAPABILITIES.promptAttachments,
] as const satisfies readonly PiWebCapability[];
const EFFECTIVE_CAPABILITY_REQUIREMENTS = {
[PI_WEB_CAPABILITIES.sessionsDeleteArchived]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.sessionsBulkMutations]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.sessionsCleanup]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.sessionsReload]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.sessionsPersistedState]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.promptAttachments]: ["web", "sessiond"],
[PI_WEB_CAPABILITIES.workspaceFileSuggestions]: ["web"],
[PI_WEB_CAPABILITIES.piPackagesManage]: ["web"],
[PI_WEB_CAPABILITIES.selectedMachineSettings]: ["web"],
} as const satisfies Record<PiWebCapability, readonly PiWebServiceComponent[]>;
export function isPiWebCapability(value: unknown): value is PiWebCapability {
@@ -25,6 +47,11 @@ export function supportsPiWebCapability(source: { capabilities?: readonly PiWebC
return source?.capabilities?.includes(capability) === true;
}
export function parseKnownPiWebCapabilities(value: unknown): PiWebCapability[] | undefined {
if (!Array.isArray(value) || !value.every((capability) => typeof capability === "string")) return undefined;
return value.filter(isPiWebCapability);
}
export function effectivePiWebCapabilities(components: Partial<Record<PiWebServiceComponent, Pick<PiWebRuntimeComponent, "available" | "capabilities">>>): PiWebCapability[] {
return KNOWN_PI_WEB_CAPABILITIES.filter((capability) => {
const requiredComponents = EFFECTIVE_CAPABILITY_REQUIREMENTS[capability];
+12
View File
@@ -1,12 +1,22 @@
export type FederatedHttpMethod = "GET" | "POST" | "PUT" | "DELETE";
export const PI_PACKAGE_MUTATION_PROXY_TIMEOUT_MS = 5 * 60_000;
export interface FederatedHttpRouteSpec {
method: FederatedHttpMethod;
path: string;
timeoutMs?: number;
}
export const FEDERATED_HTTP_ROUTES = [
{ method: "GET", path: "/pi-web/status" },
{ method: "GET", path: "/config" },
{ method: "PUT", path: "/config" },
{ method: "GET", path: "/plugins" },
{ method: "GET", path: "/pi-packages" },
{ method: "POST", path: "/pi-packages/install", timeoutMs: PI_PACKAGE_MUTATION_PROXY_TIMEOUT_MS },
{ method: "POST", path: "/pi-packages/remove", timeoutMs: PI_PACKAGE_MUTATION_PROXY_TIMEOUT_MS },
{ method: "POST", path: "/pi-packages/update", timeoutMs: PI_PACKAGE_MUTATION_PROXY_TIMEOUT_MS },
{ method: "GET", path: "/projects" },
{ method: "POST", path: "/projects" },
{ method: "DELETE", path: "/projects/:projectId" },
@@ -37,6 +47,8 @@ export const FEDERATED_HTTP_ROUTES = [
{ method: "POST", path: "/sessions" },
{ method: "POST", path: "/sessions/cleanup/preview" },
{ method: "POST", path: "/sessions/cleanup" },
{ method: "POST", path: "/sessions/bulk/archive" },
{ method: "POST", path: "/sessions/bulk/delete-archived" },
{ method: "GET", path: "/sessions/:sessionId/messages" },
{ method: "GET", path: "/sessions/:sessionId/status" },
{ method: "GET", path: "/sessions/:sessionId/models" },
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { PI_WEB_CAPABILITIES } from "./capabilities";
import { parsePiWebComponentStatus, parsePiWebInstallationInfo, parsePiWebRuntimeResponse, parsePiWebVersionResponse } from "./piWebStatusParsing";
describe("PI WEB status parsing", () => {
it("parses known top-level and component capabilities while ignoring unknown strings", () => {
expect(parsePiWebRuntimeResponse({
packageName: "@jmfederico/pi-web",
generatedAt: "now",
components: {
web: { component: "web", label: "Web/UI", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings, "future.capability"] },
sessiond: { component: "sessiond", label: "Session daemon", runtimeVersion: "1.0.0", available: true, capabilities: ["future.sessiondCapability"] },
},
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings, "future.capability"],
})).toMatchObject({
components: {
web: { capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings] },
sessiond: { capabilities: [] },
},
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings],
});
});
it("rejects runtime responses with malformed component capability arrays", () => {
expect(parsePiWebRuntimeResponse({
packageName: "@jmfederico/pi-web",
generatedAt: "now",
components: {
web: { component: "web", label: "Web/UI", available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, 1] },
sessiond: { component: "sessiond", label: "Session daemon", available: true, capabilities: [] },
},
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage],
})).toBeUndefined();
});
it("parses Docker installation metadata", () => {
expect(parsePiWebInstallationInfo({ kind: "docker", path: "/srv/pi-web-docker", dockerMode: "runtime" })).toEqual({
kind: "docker",
path: "/srv/pi-web-docker",
dockerMode: "runtime",
});
expect(parsePiWebInstallationInfo({ kind: "docker", path: "/workspace/pi-web", dockerMode: "dev" })).toEqual({
kind: "docker",
path: "/workspace/pi-web",
dockerMode: "dev",
});
});
it("ignores invalid optional Docker modes without rejecting component status", () => {
expect(parsePiWebComponentStatus({
component: "web",
label: "Web/UI",
runtimeVersion: "1.0.0",
stale: false,
available: true,
installation: { kind: "docker", path: "/workspace/pi-web", dockerMode: "hidden" },
})?.installation).toEqual({ kind: "docker", path: "/workspace/pi-web" });
});
it("parses version responses that include Docker runtime and development components", () => {
const parsed = parsePiWebVersionResponse({
packageName: "@jmfederico/pi-web",
generatedAt: "now",
components: {
web: { component: "web", label: "Web/UI", runtimeVersion: "1.0.0", stale: false, available: true, installation: { kind: "docker", path: "/srv/pi-web-docker", dockerMode: "runtime" } },
sessiond: { component: "sessiond", label: "Session daemon", runtimeVersion: "1.0.0", stale: false, available: true, installation: { kind: "docker", path: "/workspace/pi-web", dockerMode: "dev" } },
},
});
expect(parsed?.components.web.installation).toEqual({ kind: "docker", path: "/srv/pi-web-docker", dockerMode: "runtime" });
expect(parsed?.components.sessiond.installation).toEqual({ kind: "docker", path: "/workspace/pi-web", dockerMode: "dev" });
});
});
+19 -9
View File
@@ -1,5 +1,5 @@
import type { PiWebCapability, PiWebComponentStatus, PiWebInstallationInfo, PiWebRuntimeComponent, PiWebVersionResponse } from "./apiTypes.js";
import { isPiWebCapability } from "./capabilities.js";
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebVersionResponse } from "./apiTypes.js";
import { parseKnownPiWebCapabilities } from "./capabilities.js";
export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse | undefined {
if (!isRecord(value)) return undefined;
@@ -13,13 +13,26 @@ export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse
return { packageName, generatedAt, components: { web, sessiond } };
}
export function parsePiWebRuntimeResponse(value: unknown): PiWebRuntimeResponse | undefined {
if (!isRecord(value)) return undefined;
const packageName = value["packageName"];
const generatedAt = value["generatedAt"];
const components = value["components"];
const capabilities = parseKnownPiWebCapabilities(value["capabilities"]);
if (typeof packageName !== "string" || packageName === "" || typeof generatedAt !== "string" || generatedAt === "" || !isRecord(components) || capabilities === undefined) return undefined;
const web = parsePiWebRuntimeComponent(components["web"]);
const sessiond = parsePiWebRuntimeComponent(components["sessiond"]);
if (web === undefined || sessiond === undefined) return undefined;
return { packageName, generatedAt, components: { web, sessiond }, capabilities };
}
export function parsePiWebRuntimeComponent(value: unknown): PiWebRuntimeComponent | undefined {
if (!isRecord(value)) return undefined;
const component = value["component"];
const label = value["label"];
const runtimeVersion = value["runtimeVersion"];
const available = value["available"];
const capabilities = parsePiWebCapabilities(value["capabilities"]);
const capabilities = parseKnownPiWebCapabilities(value["capabilities"]);
const error = value["error"];
if (component !== "web" && component !== "sessiond") return undefined;
if (typeof label !== "string" || label === "" || typeof available !== "boolean" || capabilities === undefined) return undefined;
@@ -57,11 +70,6 @@ export function parsePiWebComponentStatus(value: unknown): PiWebComponentStatus
};
}
function parsePiWebCapabilities(value: unknown): PiWebCapability[] | undefined {
if (!Array.isArray(value) || !value.every(isPiWebCapability)) return undefined;
return value;
}
export function parsePiWebInstallationInfo(value: unknown): PiWebInstallationInfo | undefined {
if (!isRecord(value)) return undefined;
const kind = value["kind"];
@@ -69,13 +77,15 @@ export function parsePiWebInstallationInfo(value: unknown): PiWebInstallationInf
const source = value["source"];
const scope = value["scope"];
const npmRoot = value["npmRoot"];
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "unknown") return undefined;
const dockerMode = value["dockerMode"];
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "docker" && kind !== "unknown") return undefined;
return {
kind,
...(typeof path === "string" ? { path } : {}),
...(typeof source === "string" ? { source } : {}),
...(scope === "user" || scope === "project" ? { scope } : {}),
...(typeof npmRoot === "string" ? { npmRoot } : {}),
...(dockerMode === "runtime" || dockerMode === "dev" ? { dockerMode } : {}),
};
}
+9 -9
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { base64ByteLength, extensionForImageMimeType, isSupportedImageMimeType, MAX_INLINE_IMAGE_BASE64_BYTES, parsePromptAttachments } from "./promptAttachments.js";
const tinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA',".replace(/[^A-Za-z0-9+/=]/g, "");
const validImageBase64 = "QUJD";
describe("isSupportedImageMimeType", () => {
it("accepts pi-supported image types", () => {
@@ -43,12 +43,12 @@ describe("parsePromptAttachments", () => {
});
it("normalizes valid attachments", () => {
const result = parsePromptAttachments([{ kind: "image", mimeType: "image/png", data: tinyPngBase64, name: "shot.png" }]);
expect(result).toEqual([{ kind: "image", mimeType: "image/png", data: tinyPngBase64, name: "shot.png" }]);
const result = parsePromptAttachments([{ kind: "image", mimeType: "image/png", data: validImageBase64, name: "shot.png" }]);
expect(result).toEqual([{ kind: "image", mimeType: "image/png", data: validImageBase64, name: "shot.png" }]);
});
it("drops empty names", () => {
const result = parsePromptAttachments([{ kind: "image", mimeType: "image/png", data: tinyPngBase64, name: "" }]);
const result = parsePromptAttachments([{ kind: "image", mimeType: "image/png", data: validImageBase64, name: "" }]);
expect(result[0]).not.toHaveProperty("name");
});
@@ -57,9 +57,9 @@ describe("parsePromptAttachments", () => {
});
it("rejects unsupported kinds and mime types", () => {
expect(() => parsePromptAttachments([{ kind: "video", mimeType: "image/png", data: tinyPngBase64 }])).toThrow(/unsupported kind/);
expect(() => parsePromptAttachments([{ kind: "file", mimeType: "application/pdf", data: tinyPngBase64 }])).toThrow(/unsupported kind/);
expect(() => parsePromptAttachments([{ kind: "image", mimeType: "image/svg+xml", data: tinyPngBase64 }])).toThrow(/unsupported image type/);
expect(() => parsePromptAttachments([{ kind: "video", mimeType: "image/png", data: validImageBase64 }])).toThrow(/unsupported kind/);
expect(() => parsePromptAttachments([{ kind: "file", mimeType: "application/pdf", data: validImageBase64 }])).toThrow(/unsupported kind/);
expect(() => parsePromptAttachments([{ kind: "image", mimeType: "image/svg+xml", data: validImageBase64 }])).toThrow(/unsupported image type/);
});
it("accepts generic files only when file attachments are allowed", () => {
@@ -83,7 +83,7 @@ describe("parsePromptAttachments", () => {
});
it("keeps image MIME validation when file attachments are allowed", () => {
expect(() => parsePromptAttachments([{ kind: "image", mimeType: "image/svg+xml", data: tinyPngBase64 }], { allowFileAttachments: true })).toThrow(/unsupported image type/);
expect(() => parsePromptAttachments([{ kind: "image", mimeType: "image/svg+xml", data: validImageBase64 }], { allowFileAttachments: true })).toThrow(/unsupported image type/);
});
it("rejects invalid base64 data", () => {
@@ -97,7 +97,7 @@ describe("parsePromptAttachments", () => {
});
it("enforces the attachment count limit", () => {
const many = Array.from({ length: 3 }, () => ({ kind: "image", mimeType: "image/png", data: tinyPngBase64 }));
const many = Array.from({ length: 3 }, () => ({ kind: "image", mimeType: "image/png", data: validImageBase64 }));
expect(() => parsePromptAttachments(many, { maxAttachments: 2 })).toThrow(/too many attachments/);
});
});
+7 -6
View File
@@ -30,14 +30,14 @@ describe("thinkingLevels", () => {
const known = KNOWN_THINKING_LEVELS;
it("derives bar count from the available set (excluding the off level)", () => {
// 6 known levels => 5 bars.
expect(thinkingGauge("off", known).total).toBe(5);
// 7 known levels => 6 bars.
expect(thinkingGauge("off", known).total).toBe(6);
expect(thinkingGauge("off", ["off", "low", "high"]).total).toBe(2);
});
it("treats the first level as no thinking (0 filled)", () => {
expect(thinkingGauge("off", known)).toEqual({ total: 5, filled: 0 });
expect(thinkingGauge(undefined, known)).toEqual({ total: 5, filled: 0 });
expect(thinkingGauge("off", known)).toEqual({ total: 6, filled: 0 });
expect(thinkingGauge(undefined, known)).toEqual({ total: 6, filled: 0 });
});
it("fills up to the current level's rank", () => {
@@ -46,6 +46,7 @@ describe("thinkingLevels", () => {
expect(thinkingGauge("medium", known).filled).toBe(3);
expect(thinkingGauge("high", known).filled).toBe(4);
expect(thinkingGauge("xhigh", known).filled).toBe(5);
expect(thinkingGauge("max", known).filled).toBe(6);
});
it("adapts to a runtime-provided set of a different size", () => {
@@ -56,8 +57,8 @@ describe("thinkingLevels", () => {
});
it("falls back to the known set when no usable available set is given", () => {
expect(thinkingGauge("high", [])).toEqual({ total: 5, filled: 4 });
expect(thinkingGauge("high", ["only-one"])).toEqual({ total: 5, filled: 4 });
expect(thinkingGauge("high", [])).toEqual({ total: 6, filled: 4 });
expect(thinkingGauge("high", ["only-one"])).toEqual({ total: 6, filled: 4 });
});
it("fills 0 for an unknown current level instead of throwing", () => {
+1 -1
View File
@@ -13,7 +13,7 @@ export type { ThinkingLevel };
* either breaks, update this list and give the new level a label/description
* where thinking levels are presented.
*/
export const KNOWN_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const satisfies readonly ThinkingLevel[];
export const KNOWN_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
export function isKnownThinkingLevel(value: string): value is ThinkingLevel {
return KNOWN_THINKING_LEVELS.some((level) => level === value);