Compare commits

..
2 Commits
18 changed files with 187 additions and 80 deletions
@@ -134,7 +134,7 @@ const HOSTED_TOOLS = {
name: "code_interpreter",
input: (item) => ({ code: item.code, container_id: item.container_id }),
},
computer_use_call: { name: "computer_use", input: (item) => item.action ?? {} },
computer_call: { name: "computer_use", input: (item) => item.action ?? {} },
image_generation_call: { name: "image_generation", input: () => ({}), result: hostedToolResult },
mcp_call: {
name: "mcp",
@@ -28,7 +28,11 @@ export const ResponseIncludables = [
export type ResponseIncludable = (typeof ResponseIncludables)[number] | (string & {})
export const ServiceTiers = ["auto", "default", "flex", "priority"] as const
export type ServiceTier = (typeof ServiceTiers)[number]
export type ServiceTier = (typeof ServiceTiers)[number] | (string & {})
export const ServiceTier = Schema.declare<ServiceTier>(
(value): value is ServiceTier => typeof value === "string",
{ title: "ServiceTier" },
)
export const Truncations = ["auto", "disabled"] as const
export type Truncation = (typeof Truncations)[number]
@@ -38,7 +42,7 @@ export const ResponseIncludableSchema = Schema.declare<ResponseIncludable>(
(value): value is ResponseIncludable => typeof value === "string",
{ title: "ResponseIncludable" },
)
export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export const ServiceTierSchema = ServiceTier
export const TruncationSchema = Schema.Literals(Truncations)
export const AllowedTools = Schema.Struct({
@@ -9,8 +9,8 @@ export type OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity
// in lockstep with `openai-node/src/resources/responses/responses.ts`.
export const OpenAIResponseIncludables = OpenResponsesOptions.ResponseIncludables
export type OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludable
export const OpenAIServiceTiers = OpenResponsesOptions.ServiceTiers
export type OpenAIServiceTier = OpenResponsesOptions.ServiceTier
export const OpenAIServiceTiers = [...OpenResponsesOptions.ServiceTiers, "scale"] as const
export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] | (string & {})
export const OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort
export const OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity
+6 -2
View File
@@ -1,9 +1,13 @@
import { mergeProviderOptions, type ProviderOptions } from "../schema/index.js"
import type { OpenResponsesOptionsInput } from "./open-responses-options.js"
import type { OpenAIServiceTier } from "../protocols/utils/openai-options.js"
import type { Options } from "../protocols/utils/open-responses-options.js"
export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options.js"
export type OpenAIOptionsInput = OpenResponsesOptionsInput
export type OpenAIOptionsInput = Omit<Options, "serviceTier"> & {
readonly serviceTier?: OpenAIServiceTier
readonly [key: string]: unknown
}
export type OpenAIProviderOptionsInput = OpenAIOptionsInput
@@ -4,10 +4,4 @@ import { GoogleVertexChat } from "../../src/providers.js"
const model = GoogleVertexChat.configure({ accessToken: "test", project: "project" }).model("gemini")
LLM.request({ model, prompt: "Hello", providerOptions: { serviceTier: "priority" } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Vertex OpenAI-compatible service tiers use the OpenAI union.
providerOptions: { serviceTier: "premium" },
})
LLM.request({ model, prompt: "Hello", providerOptions: { serviceTier: "future-tier" } })
@@ -8,6 +8,8 @@ LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffo
LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "low" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "scale" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "future-tier" } })
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } })
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
@@ -189,6 +189,7 @@ describe("Open Responses-compatible route", () => {
streamOptions: { includeObfuscation: false },
topLogprobs: 3,
truncation: "auto",
serviceTier: "provider-tier",
allowedTools: { toolNames: ["lookup"] },
maxToolCalls: 2,
parallelToolCalls: false,
@@ -213,6 +214,7 @@ describe("Open Responses-compatible route", () => {
presence_penalty: 0.2,
frequency_penalty: -0.1,
truncation: "auto",
service_tier: "provider-tier",
tool_choice: {
type: "allowed_tools",
mode: "auto",
@@ -188,13 +188,13 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("omits unsupported semantic service tiers", () =>
it.effect("passes through provider-defined service tiers", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLMRequest.update(request, { providerOptions: { serviceTier: "unsupported" } }),
LLMRequest.update(request, { providerOptions: { serviceTier: "scale" } }),
)
expect(prepared.body).not.toHaveProperty("service_tier")
expect(prepared.body.service_tier).toBe("scale")
}),
)
@@ -2652,6 +2652,47 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("decodes computer_call as provider-executed tool-call + tool-result", () =>
Effect.gen(function* () {
const item = {
type: "computer_call",
id: "computer_1",
call_id: "call_1",
status: "completed",
action: { type: "click", x: 100, y: 200 },
}
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{ type: "response.output_item.done", item },
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
),
),
),
)
expect(response.events.filter((event) => event.type === "tool-call" || event.type === "tool-result")).toEqual([
{
type: "tool-call",
id: "computer_1",
name: "computer_use",
input: { type: "click", x: 100, y: 200 },
providerExecuted: true,
providerMetadata: { openai: { itemId: "computer_1" } },
},
{
type: "tool-result",
id: "computer_1",
name: "computer_use",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "computer_1" } },
},
])
}),
)
it.effect("decodes image generation output as image content", () =>
Effect.gen(function* () {
const item = {
@@ -131,8 +131,11 @@ test("animates review and terminal panels while caching hidden terminal content"
await resetTerminalBottomMotion(page)
await resetTerminalAnchorGaps(page)
await resetPanelGaps(page)
const reviewContent = page.locator('[data-component="session-review-v2"]')
await reviewContent.evaluate((element) => element.setAttribute("data-cache-probe", "original"))
await reviewToggle.click()
await expect(page.locator("#review-panel")).toHaveCount(0)
await expect(page.locator("#review-panel")).toBeHidden()
await expect(reviewContent).toHaveAttribute("data-cache-probe", "original")
await expect(panel).toBeVisible()
await expectHeightMotions(page, "session-side-region", 2)
await expectHeightMotions(page, "session-side-terminal-region", 2)
@@ -142,6 +145,7 @@ test("animates review and terminal panels while caching hidden terminal content"
await expectPanelGapHeld(page)
await reviewToggle.click()
await expect(page.locator("#review-panel")).toBeVisible()
await expect(reviewContent).toHaveAttribute("data-cache-probe", "original")
await expectHeightMotions(page, "session-side-region", 3)
await expectHeightMotions(page, "session-side-terminal-region", 3)
@@ -161,14 +165,18 @@ test("animates review and terminal panels while caching hidden terminal content"
await reviewToggle.click()
await expect(page.locator("#review-panel")).toHaveCount(0)
await expectWidthMotions(page, 2)
await expectSideSlideSettled(page, 2)
await expectHiddenSideAligned(page)
await resetHeightMotions(page)
await resetHorizontalScrolls(page)
await page.keyboard.press("Control+Backquote")
await expect(panel).toHaveAttribute("aria-hidden", "false")
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
await expectWidthMotions(page, 3)
await expectSideMotionSettled(page)
await expectSideSlideSettled(page, 3)
await expectNoHeightMotion(page)
await expectNoHorizontalScroll(page)
await page.keyboard.press("Control+Backquote")
await expect(panel).toBeHidden()
@@ -209,6 +217,8 @@ test("animates review and terminal panels while caching hidden terminal content"
type MotionProbe = {
widths: number
widthEnds: number
horizontalScrolls: number[]
reviewWidths: number[]
paintGaps: { review: number; terminalSurface: number }[]
terminalContentSizes: { width: number; height: number }[]
@@ -225,6 +235,8 @@ async function installMotionProbe(page: Page) {
await page.evaluate(() => {
const probe: MotionProbe = {
widths: 0,
widthEnds: 0,
horizontalScrolls: [],
reviewWidths: [],
paintGaps: [],
terminalContentSizes: [],
@@ -299,10 +311,16 @@ async function installMotionProbe(page: Page) {
probe.heights.push(slot)
}
})
document.addEventListener("transitionend", (event) => {
if (!(event.target instanceof Element)) return
if (event.propertyName === "width" && event.target.getAttribute("data-slot") === "session-chat-panel")
probe.widthEnds++
})
document.addEventListener("animationstart", (event) => {
if (!(event.target instanceof Element) || event.target.getAttribute("data-component") !== "terminal-panel") return
probe.animations.push(event.animationName)
})
window.addEventListener("scroll", () => probe.horizontalScrolls.push(window.scrollX))
;(window as Window & { __panelMotion?: MotionProbe }).__panelMotion = probe
})
}
@@ -320,11 +338,10 @@ async function resetHeightMotions(page: Page) {
})
}
async function expectSideMotionSettled(page: Page) {
const side = page.locator('[data-slot="session-side-panel-presence"]')
async function expectSideSlideSettled(page: Page, count: number) {
await expect
.poll(() => side.evaluate((element) => element.getAnimations().every((item) => item.playState === "finished")))
.toBe(true)
.poll(() => page.evaluate(() => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.widthEnds ?? 0))
.toBeGreaterThanOrEqual(count)
}
async function expectNoHeightMotion(page: Page) {
@@ -334,6 +351,40 @@ async function expectNoHeightMotion(page: Page) {
expect(heights).toEqual([])
}
async function expectHiddenSideAligned(page: Page) {
await expect
.poll(() =>
page.evaluate(() => {
const chat = document.querySelector<HTMLElement>('[data-slot="session-chat-panel"]')
const side = document.querySelector<HTMLElement>('[data-slot="session-side-panel-presence"]')
if (!chat?.parentElement || !side) return Number.POSITIVE_INFINITY
const row = chat.parentElement.getBoundingClientRect()
const hidden = side.getBoundingClientRect()
return Math.max(
Math.abs(row.top - hidden.top),
Math.abs(row.right - hidden.right),
Math.abs(row.bottom - hidden.bottom),
)
}),
)
.toBeLessThanOrEqual(1)
}
async function resetHorizontalScrolls(page: Page) {
await page.evaluate(() => {
const probe = (window as Window & { __panelMotion?: MotionProbe }).__panelMotion
if (probe) probe.horizontalScrolls = []
})
}
async function expectNoHorizontalScroll(page: Page) {
const scrolls = await page.evaluate(
() => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.horizontalScrolls ?? [],
)
expect(Math.max(0, ...scrolls)).toBe(0)
expect(await page.evaluate(() => window.scrollX)).toBe(0)
}
async function expectReviewWidthStable(page: Page) {
const side = page.locator('[data-slot="session-side-panel-presence"]')
await expect
+2 -2
View File
@@ -101,11 +101,11 @@ export const focusTerminalById = (id: string) => {
const textarea = terminal.querySelector("textarea")
if (textarea instanceof HTMLTextAreaElement) {
textarea.focus()
textarea.focus({ preventScroll: true })
return true
}
terminal.focus()
terminal.focus({ preventScroll: true })
terminal.dispatchEvent(
typeof PointerEvent === "function"
? new PointerEvent("pointerdown", { bubbles: true, cancelable: true })
+2 -1
View File
@@ -165,7 +165,7 @@ export function SessionScreen(props: { session: SessionModel }) {
<>
<SessionHeader />
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2">
<div ref={screen.panel.ref} class="relative flex-1 min-h-0 flex flex-col md:flex-row gap-2">
<div
classList={{
"@container relative z-10 shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
@@ -241,6 +241,7 @@ export function SessionScreen(props: { session: SessionModel }) {
if (event.currentTarget !== event.target) return
if (event.animationName !== "side-region-presence-out") return
if (screen.side.region.open()) return
if (sideTerminalVisible()) return
setStore("sideRegionPresent", false)
setStore("sideReviewPresent", false)
}}
@@ -347,9 +347,9 @@ export const Terminal = (props: TerminalProps) => {
const focusTerminal = () => {
const t = term
if (!t) return
t.focus()
t.textarea?.focus()
setTimeout(() => t.textarea?.focus(), 0)
const focus = () => (t.textarea ? t.textarea.focus({ preventScroll: true }) : t.focus())
focus()
setTimeout(focus, 0)
}
const handlePointerDown = () => {
const activeElement = document.activeElement
+1 -1
View File
@@ -197,7 +197,7 @@ const evaluateShell = Effect.fnUntraced(function* (
) {
const matches = Array.from(text.matchAll(shellRegex))
if (matches.length === 0) return text
const shell = yield* services.shell.resolve({ preference: "compatible" })
const shell = yield* services.shell.preferred()
const outputs = yield* Effect.forEach(
matches,
(match) => {
+1 -1
View File
@@ -164,7 +164,7 @@ const layer = () =>
const create = Effect.fn("Pty.create")(function* (input: CreateInput) {
const id = PtyID.ascending()
const command = input.command || (yield* shell.resolve({ preference: "configured" }))
const command = input.command || (yield* shell.preferred())
const args = ShellSelect.login(command) ? [...(input.args ?? []), "-l"] : [...(input.args ?? [])]
const cwd = input.cwd || location.directory
const env = {
+2 -2
View File
@@ -185,7 +185,7 @@ const layer = () =>
return session.info
})
const name = () => shell.resolve({ preference: "compatible" }).pipe(Effect.map(ShellSelect.name))
const name = () => shell.preferred().pipe(Effect.map(ShellSelect.name))
const output = Effect.fnUntraced(function* (id: Shell.ID, input?: Shell.OutputInput) {
const session = yield* require(id)
@@ -230,7 +230,7 @@ const layer = () =>
command: input.command,
cwd: input.cwd ?? location.directory,
timeout: input.timeout,
shell: yield* shell.resolve({ preference: "compatible" }),
shell: yield* shell.preferred(),
env: {
...(sessionEnvironment ?? process.env),
TERM: "xterm-256color",
+33 -27
View File
@@ -41,12 +41,8 @@ export type Draft = {
configure: (shell: string) => void
}
export type ResolveInput = {
preference: "configured" | "compatible"
}
export interface Interface extends State.Transformable<Draft> {
readonly resolve: (input: ResolveInput) => Effect.Effect<string>
readonly preferred: () => Effect.Effect<string>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ShellSelect") {}
@@ -74,7 +70,7 @@ function meta(file: string) {
return META[name(file)]
}
function compatible(file: string) {
function ok(file: string) {
return meta(file)?.deny !== true
}
@@ -82,7 +78,7 @@ function rooted(file: string) {
return path.isAbsolute(FSUtil.windowsPath(file))
}
function executable(file: string, options?: Options, bin?: string) {
function resolve(file: string, options?: Options, bin?: string) {
const shell = full(file, options, bin)
if (rooted(shell)) {
if (stat(shell)?.isFile()) return shell
@@ -112,9 +108,9 @@ async function unix() {
return ["/bin/bash", "/bin/zsh", "/bin/sh"]
}
function select(file: string | undefined, options?: Options, opts?: { compatible?: boolean }, bin?: string) {
if (file && (!opts?.compatible || compatible(file))) {
const shell = executable(file, options, bin)
function select(file: string | undefined, options?: Options, opts?: { acceptable?: boolean }, bin?: string) {
if (file && (!opts?.acceptable || ok(file))) {
const shell = resolve(file, options, bin)
if (shell) return shell
}
if (process.platform === "win32") return win(options, bin)[0]
@@ -155,8 +151,8 @@ function info(file: string, options?: Options, bin?: string): Item {
const n = name(item)
return {
path: item,
name: executable(n, options, bin) ? n : item,
acceptable: compatible(item),
name: resolve(n, options, bin) ? n : item,
acceptable: ok(item),
}
}
@@ -167,28 +163,38 @@ export function args(file: string, command: string) {
return ["-c", command]
}
let defaultConfigured: { bin?: string; value: string } | undefined
let defaultCompatible: { bin?: string; value: string } | undefined
let defaultPreferred: { bin?: string; value: string } | undefined
let defaultAcceptable: { bin?: string; value: string } | undefined
export function resolve(input: ResolveInput, configShell?: string, options?: Options, bin?: string) {
const filter = input.preference === "compatible" ? { compatible: true } : undefined
if (configShell) return select(configShell, options, filter, bin)
if (options?.gitbash) return select(process.env.SHELL, options, filter, bin)
const cached = input.preference === "compatible" ? defaultCompatible : defaultConfigured
export function preferred(configShell?: string, options?: Options, bin?: string) {
if (configShell) return select(configShell, options, undefined, bin)
if (options?.gitbash) return select(process.env.SHELL, options, undefined, bin)
const cached = defaultPreferred
if (cached && cached.bin === bin) return cached.value
const value = select(process.env.SHELL, undefined, filter, bin) ?? fallback(bin)
if (input.preference === "compatible") defaultCompatible = { bin, value }
if (input.preference === "configured") defaultConfigured = { bin, value }
const value = select(process.env.SHELL, undefined, undefined, bin) ?? fallback(bin)
defaultPreferred = { bin, value }
return value
}
resolve.reset = () => {
defaultConfigured = undefined
defaultCompatible = undefined
preferred.reset = () => {
defaultPreferred = undefined
}
export function acceptable(configShell?: string, options?: Options, bin?: string) {
if (configShell) return select(configShell, options, { acceptable: true }, bin)
if (options?.gitbash) return select(process.env.SHELL, options, { acceptable: true }, bin)
const cached = defaultAcceptable
if (cached && cached.bin === bin) return cached.value
const value = select(process.env.SHELL, undefined, { acceptable: true }, bin) ?? fallback(bin)
defaultAcceptable = { bin, value }
return value
}
acceptable.reset = () => {
defaultAcceptable = undefined
}
export async function list(options?: Options, bin?: string): Promise<Item[]> {
const shells = process.platform === "win32" ? win(options, bin) : await unix()
return shells.filter((shell) => executable(shell, options, bin)).map((shell) => info(shell, options, bin))
return shells.filter((shell) => resolve(shell, options, bin)).map((shell) => info(shell, options, bin))
}
const layer = (options?: Options) =>
@@ -208,7 +214,7 @@ const layer = (options?: Options) =>
return Service.of({
transform: state.transform,
reload: state.reload,
resolve: (input) => Effect.sync(() => resolve(input, state.get().shell, options, global.bin)),
preferred: () => Effect.sync(() => preferred(state.get().shell, options, global.bin)),
})
}),
)
+2 -2
View File
@@ -24,12 +24,12 @@ describe("ConfigShellPlugin.Plugin", () => {
yield* ConfigShellPlugin.Plugin.effect(yield* PluginHost.make(plugins))
const configured = process.platform === "win32" ? FSUtil.windowsPath(process.execPath) : process.execPath
expect(yield* shell.resolve({ preference: "configured" })).toBe(configured)
expect(yield* shell.preferred()).toBe(configured)
yield* config.setEntries([])
yield* bus.publish(Event.Updated, {})
for (let attempt = 0; attempt < 200; attempt++) {
if ((yield* shell.resolve({ preference: "configured" })) !== configured) return
if ((yield* shell.preferred()) !== configured) return
yield* Effect.sleep("10 millis")
}
yield* Effect.die(new Error("Timed out waiting for shell config reload"))
+20 -18
View File
@@ -8,13 +8,15 @@ const withShell = async (shell: string | undefined, fn: () => void | Promise<voi
const prev = process.env.SHELL
if (shell === undefined) delete process.env.SHELL
else process.env.SHELL = shell
ShellSelect.resolve.reset()
ShellSelect.acceptable.reset()
ShellSelect.preferred.reset()
try {
await fn()
} finally {
if (prev === undefined) delete process.env.SHELL
else process.env.SHELL = prev
ShellSelect.resolve.reset()
ShellSelect.acceptable.reset()
ShellSelect.preferred.reset()
}
}
@@ -34,16 +36,16 @@ describe("shell", () => {
test("falls back when configured shell cannot be resolved", async () => {
await withShell(undefined, async () => {
const configured = ShellSelect.resolve({ preference: "configured" })
const compatible = ShellSelect.resolve({ preference: "compatible" })
expect(ShellSelect.resolve({ preference: "configured" }, "opencode-missing-shell")).toBe(configured)
expect(ShellSelect.resolve({ preference: "compatible" }, "opencode-missing-shell")).toBe(compatible)
const preferred = ShellSelect.preferred()
const acceptable = ShellSelect.acceptable()
expect(ShellSelect.preferred("opencode-missing-shell")).toBe(preferred)
expect(ShellSelect.acceptable("opencode-missing-shell")).toBe(acceptable)
})
})
test("falls back for terminal-only shells when compatibility is required", () => {
expect(ShellSelect.name(ShellSelect.resolve({ preference: "compatible" }, "fish"))).not.toBe("fish")
expect(ShellSelect.name(ShellSelect.resolve({ preference: "compatible" }, "nu"))).not.toBe("nu")
test("falls back for terminal-only acceptable shells", () => {
expect(ShellSelect.name(ShellSelect.acceptable("fish"))).not.toBe("fish")
expect(ShellSelect.name(ShellSelect.acceptable("nu"))).not.toBe("nu")
})
test("builds command args per shell family", () => {
@@ -63,14 +65,14 @@ describe("shell", () => {
if (process.platform === "win32") {
test("rejects blacklisted shells case-insensitively", async () => {
await withShell("NU.EXE", async () => {
expect(ShellSelect.name(ShellSelect.resolve({ preference: "compatible" }))).not.toBe("nu")
expect(ShellSelect.name(ShellSelect.acceptable())).not.toBe("nu")
})
})
test("normalizes Git Bash shell paths from env", async () => {
const shell = "/cygdrive/c/Program Files/Git/bin/bash.exe"
await withShell(shell, async () => {
expect(ShellSelect.resolve({ preference: "configured" })).toBe(FSUtil.windowsPath(shell))
expect(ShellSelect.preferred()).toBe(FSUtil.windowsPath(shell))
})
})
@@ -78,19 +80,19 @@ describe("shell", () => {
const bash = ShellSelect.gitbash()
if (!bash) return
await withShell("/usr/bin/bash", async () => {
expect(ShellSelect.resolve({ preference: "compatible" })).toBe(bash)
expect(ShellSelect.resolve({ preference: "configured" })).toBe(bash)
expect(ShellSelect.acceptable()).toBe(bash)
expect(ShellSelect.preferred()).toBe(bash)
})
})
test("resolves bare bash to Git Bash before PATH", async () => {
const bash = ShellSelect.gitbash()
if (!bash) return
expect(ShellSelect.resolve({ preference: "compatible" }, "bash")).toBe(bash)
expect(ShellSelect.resolve({ preference: "configured" }, "bash")).toBe(bash)
expect(ShellSelect.acceptable("bash")).toBe(bash)
expect(ShellSelect.preferred("bash")).toBe(bash)
await withShell("bash", async () => {
expect(ShellSelect.resolve({ preference: "compatible" })).toBe(bash)
expect(ShellSelect.resolve({ preference: "configured" })).toBe(bash)
expect(ShellSelect.acceptable()).toBe(bash)
expect(ShellSelect.preferred()).toBe(bash)
})
})
@@ -98,7 +100,7 @@ describe("shell", () => {
const shell = which("pwsh") || which("powershell")
if (!shell) return
await withShell(path.win32.basename(shell), async () => {
expect(ShellSelect.resolve({ preference: "configured" })).toBe(shell)
expect(ShellSelect.preferred()).toBe(shell)
})
})
}