feat(web): open the local settings file
This commit is contained in:
@@ -47,18 +47,35 @@ function expectErr<T>(response: RpcResponse<T>): { code: string; message: string
|
||||
class MemorySettings extends Settings {
|
||||
doc: Record<string, unknown>
|
||||
|
||||
constructor(ctx: ConstructorParameters<typeof Settings>[0], options?: { doc?: Record<string, unknown>; readOnly?: boolean }) {
|
||||
constructor(ctx: ConstructorParameters<typeof Settings>[0], options?: {
|
||||
doc?: Record<string, unknown>
|
||||
readOnly?: boolean
|
||||
documentPath?: string
|
||||
preparedPath?: string
|
||||
}) {
|
||||
super(ctx)
|
||||
this.doc = structuredClone(options?.doc ?? {})
|
||||
this.readOnly = options?.readOnly ?? false
|
||||
this.path = options?.documentPath
|
||||
this.preparedPath = options?.preparedPath
|
||||
}
|
||||
|
||||
private readonly readOnly: boolean
|
||||
private readonly path: string | undefined
|
||||
private readonly preparedPath: string | undefined
|
||||
|
||||
get writable(): boolean {
|
||||
return !this.readOnly
|
||||
}
|
||||
|
||||
override get documentPath(): string | undefined {
|
||||
return this.path
|
||||
}
|
||||
|
||||
override prepareDocument(): Promise<string | undefined> {
|
||||
return Promise.resolve(this.preparedPath ?? this.documentPath)
|
||||
}
|
||||
|
||||
protected load(): Promise<Record<string, unknown>> {
|
||||
return Promise.resolve(structuredClone(this.doc))
|
||||
}
|
||||
@@ -146,7 +163,12 @@ const AdapterConfig = z.object({
|
||||
})
|
||||
|
||||
async function harness(options?: {
|
||||
settings?: false | { doc?: Record<string, unknown>; readOnly?: boolean }
|
||||
settings?: false | {
|
||||
doc?: Record<string, unknown>
|
||||
readOnly?: boolean
|
||||
documentPath?: string
|
||||
preparedPath?: string
|
||||
}
|
||||
credentials?: false | { shadowed?: string[] }
|
||||
/** Skip the directory registration to exercise a namespace the proxy does not expose. */
|
||||
configurableProviders?: false
|
||||
@@ -205,11 +227,15 @@ describe('settings domain', () => {
|
||||
})
|
||||
|
||||
it('describes layered redacted namespaces with their secret slots', async () => {
|
||||
const ctx = await harness({ settings: { doc: { 'llm-deepseek': { apiKey: 'user-secret', baseURL: 'https://user' } } } })
|
||||
const ctx = await harness({ settings: {
|
||||
doc: { 'llm-deepseek': { apiKey: 'user-secret', baseURL: 'https://user' } },
|
||||
documentPath: '/tmp/custom-settings.yaml',
|
||||
} })
|
||||
ctx.settings.register(NS, AdapterConfig, { base: { baseURL: 'https://base' } })
|
||||
const api = createApiProxy(ctx, DEFAULTS)
|
||||
const value = expectOk(await api.settings.describe(request({})))
|
||||
expect(value.writable).toBe(true)
|
||||
expect(value.documentPath).toBe('/tmp/custom-settings.yaml')
|
||||
expect(value.namespaces).toHaveLength(1)
|
||||
const view = value.namespaces[0]!
|
||||
expect(view.ns).toBe('llm-deepseek')
|
||||
@@ -222,6 +248,33 @@ describe('settings domain', () => {
|
||||
expect(JSON.stringify(value)).not.toContain('user-secret')
|
||||
})
|
||||
|
||||
it('opens the provider-resolved document without accepting a browser path', async () => {
|
||||
const ctx = await harness({ settings: {
|
||||
documentPath: '/tmp/described-settings.yaml',
|
||||
preparedPath: '/tmp/custom-settings.yaml',
|
||||
} })
|
||||
const opened: string[] = []
|
||||
const api = createApiProxy(ctx, {
|
||||
...DEFAULTS,
|
||||
openTextFile: (path) => {
|
||||
opened.push(path)
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
|
||||
expect(expectOk(await api.settings.openDocument(request({}), new AbortController().signal)))
|
||||
.toEqual({ opened: true })
|
||||
expect(opened).toEqual(['/tmp/custom-settings.yaml'])
|
||||
})
|
||||
|
||||
it('refuses to open settings when the provider has no local document', async () => {
|
||||
const ctx = await harness()
|
||||
const api = createApiProxy(ctx, DEFAULTS)
|
||||
const error = expectErr(await api.settings.openDocument(request({}), new AbortController().signal))
|
||||
expect(error.code).toBe('internal')
|
||||
expect(error.message).toContain('no local document')
|
||||
})
|
||||
|
||||
it('serves model-provider and explicitly allowlisted Web namespaces only', async () => {
|
||||
// The settings seam is general: any plugin may register a namespace for
|
||||
// its own configuration. The Web configuration plane remains opt-in, so a
|
||||
|
||||
Reference in New Issue
Block a user