Files
deepseek-harness/packages/bash/pwsh-local/tests/settings.spec.ts
Yichen Jiang ddc1ec2bd2 feat(bash): resolve executor config through the bash settings namespace
The capability's namespace is owned by the seam because it names the
capability, not an implementation: a host composes exactly one provider of
ctx.bash, so both executor families register the same namespace with their
own schema and composition entry without ever colliding, and a settings
document carried between platforms keeps resolving on both.

Both executors read their config through a source thunk, so a stored change
reaches the next command. The constructor checks the schema cannot express
become the section validator, refusing a bad value at the write instead of
at the next command. pwsh re-resolves its executable only when the declared
path changed, so an unrelated settings change never re-probes the filesystem.
2026-08-10 16:10:33 +08:00

109 lines
3.9 KiB
TypeScript

/** The shared `bash` settings section as the pwsh executor family resolves it. */
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { Fiber } from 'cordis'
import { Settings } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { BASH_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-bash'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
/** The smallest real provider: one in-memory document, always writable. */
class MemorySettings extends Settings {
doc: Record<string, unknown> = {}
get writable(): boolean {
return true
}
protected load(): Promise<Record<string, unknown>> {
return Promise.resolve(structuredClone(this.doc))
}
protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
this.doc = { ...this.doc, [ns]: structuredClone(section) }
return Promise.resolve()
}
}
async function boot(config: ConstructorParameters<typeof PwshLocalExecutor>[1] = {}): Promise<{
ctx: Context
settingsFiber: Fiber
executorFiber: Fiber
pwsh: PwshLocalExecutor
}> {
const ctx = new Context()
await ctx.plugin(LocalSubprocessService)
const settingsFiber = ctx.plugin(MemorySettings)
await settingsFiber.await()
const executorFiber = ctx.plugin(PwshLocalExecutor, { timeoutMs: 60_000, ...config })
await executorFiber.await()
return { ctx, settingsFiber, executorFiber, pwsh: ctx.bash as PwshLocalExecutor }
}
describe('pwsh executor over the bash settings section', () => {
it('resolves the user layer over the composition entry', async () => {
const bench = await boot()
expect(bench.pwsh.config.timeoutMs).toBe(60_000)
await bench.ctx.settings.update(BASH_SETTINGS_NAMESPACE, { timeoutMs: 5_000 })
expect(bench.pwsh.config.timeoutMs).toBe(5_000)
await bench.ctx.fiber.dispose()
})
it('refuses a stored value the constructor would have rejected', async () => {
const bench = await boot()
await expect(bench.ctx.settings.update(BASH_SETTINGS_NAMESPACE, { timeoutMs: 0 }))
.rejects.toThrow(/pwsh-local: timeoutMs must be a positive finite number/)
expect(bench.pwsh.config.timeoutMs).toBe(60_000)
await bench.ctx.fiber.dispose()
})
it('re-resolves the executable when the stored path changes', async () => {
const bench = await boot({ pwshPath: '/opt/first/pwsh' })
expect(bench.pwsh.pwshPath).toBe('/opt/first/pwsh')
await bench.ctx.settings.update(BASH_SETTINGS_NAMESPACE, { pwshPath: '/opt/second/pwsh' })
expect(bench.pwsh.pwshPath).toBe('/opt/second/pwsh')
await bench.ctx.fiber.dispose()
})
it('keeps the resolved executable when an unrelated field changes', async () => {
const bench = await boot({ pwshPath: '/opt/first/pwsh' })
const before = bench.pwsh.pwshPath
await bench.ctx.settings.update(BASH_SETTINGS_NAMESPACE, { timeoutMs: 5_000 })
expect(bench.pwsh.pwshPath).toBe(before)
await bench.ctx.fiber.dispose()
})
it('falls back to the composition entry when the settings provider detaches', async () => {
const bench = await boot({ pwshPath: '/opt/first/pwsh' })
await bench.ctx.settings.update(BASH_SETTINGS_NAMESPACE, { timeoutMs: 5_000, pwshPath: '/opt/second/pwsh' })
expect(bench.pwsh.config.timeoutMs).toBe(5_000)
expect(bench.pwsh.pwshPath).toBe('/opt/second/pwsh')
await bench.settingsFiber.dispose()
expect(bench.pwsh.config.timeoutMs).toBe(60_000)
expect(bench.pwsh.pwshPath).toBe('/opt/first/pwsh')
await bench.ctx.fiber.dispose()
})
it('releases the namespace when the executor unloads', async () => {
const bench = await boot()
expect(bench.ctx.settings.describe().map(row => String(row.ns))).toContain('bash')
await bench.executorFiber.dispose()
expect(bench.ctx.settings.describe().map(row => String(row.ns))).not.toContain('bash')
await bench.ctx.fiber.dispose()
})
})