|
|
@@ -20,6 +20,8 @@ interface FakeOptions {
|
|
|
calls?: Recorded[]
|
|
|
/** Reject `list` with this message. */
|
|
|
failList?: string
|
|
|
+ /** Reject only this numbered `list` call. */
|
|
|
+ failListAt?: number
|
|
|
/** Reject `read` with this message. */
|
|
|
failRead?: string
|
|
|
/** Reject `copy` with this message. */
|
|
|
@@ -38,6 +40,14 @@ interface FakeOptions {
|
|
|
failCapability?: string
|
|
|
/** Hold `remove` until this resolves, to observe the in-flight state. */
|
|
|
holdRemove?: Promise<void>
|
|
|
+ /** Initial new-session picker visibility. */
|
|
|
+ showPicker?: boolean
|
|
|
+ /** Mutable Host policy used when a Settings write is reflected by the roster. */
|
|
|
+ pickerPolicy?: { enabled: boolean }
|
|
|
+ /** Simulate a concurrent Host write winning after this client's policy write. */
|
|
|
+ ignorePickerWrite?: boolean
|
|
|
+ /** Throw from the Settings transport instead of returning a Remote failure. */
|
|
|
+ throwSettings?: unknown
|
|
|
}
|
|
|
|
|
|
const remoteOk = (value: unknown) => Promise.resolve({ ok: true as const, value })
|
|
|
@@ -58,18 +68,26 @@ function fakeCtx(
|
|
|
options: FakeOptions = {},
|
|
|
): ClientContext {
|
|
|
const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) }
|
|
|
+ let listCount = 0
|
|
|
return {
|
|
|
remote: {
|
|
|
agentPresets: {
|
|
|
list: () => {
|
|
|
record('list', {})
|
|
|
- if (options.failList !== undefined) return remoteFail(options.failList)
|
|
|
+ listCount += 1
|
|
|
+ if (options.failList !== undefined
|
|
|
+ && (options.failListAt === undefined || options.failListAt === listCount)) {
|
|
|
+ return remoteFail(options.failList)
|
|
|
+ }
|
|
|
+ const selectionEnabled = options.pickerPolicy?.enabled ?? options.showPicker ?? true
|
|
|
+ const effectiveDefault = selectionEnabled ? defaultId.id : 'standard'
|
|
|
return remoteOk({
|
|
|
presets: [...presets].map(([id, preset]) => ({
|
|
|
- id, trust: preset.trust, isDefault: id === defaultId.id,
|
|
|
+ id, trust: preset.trust, isDefault: id === effectiveDefault,
|
|
|
...preset.name === undefined ? {} : { name: preset.name },
|
|
|
})),
|
|
|
authorable: options.authorable ?? true,
|
|
|
+ modeSelectionEnabled: selectionEnabled,
|
|
|
})
|
|
|
},
|
|
|
read: (agentPreset: string) => {
|
|
|
@@ -114,19 +132,23 @@ function fakeCtx(
|
|
|
},
|
|
|
},
|
|
|
settings: {
|
|
|
+ update: (ns: string, patch: { default?: unknown; modeSelectionEnabled?: unknown }) => {
|
|
|
+ record('settings.update', { ns, patch })
|
|
|
+ if (options.throwSettings !== undefined) throw options.throwSettings
|
|
|
+ if (options.failSettings !== undefined) return remoteFail(options.failSettings)
|
|
|
+ if (typeof patch.default === 'string') defaultId.id = patch.default
|
|
|
+ if (typeof patch.modeSelectionEnabled === 'boolean' && !options.ignorePickerWrite) {
|
|
|
+ const policy = options.pickerPolicy ?? { enabled: options.showPicker ?? true }
|
|
|
+ policy.enabled = patch.modeSelectionEnabled
|
|
|
+ }
|
|
|
+ return remoteOk({})
|
|
|
+ },
|
|
|
canOpenAgentPresetDirectory: () => {
|
|
|
record('canOpenAgentPresetDirectory', {})
|
|
|
return options.failCapability === undefined
|
|
|
? remoteOk(options.hasDocument ?? true)
|
|
|
: remoteFail(options.failCapability)
|
|
|
},
|
|
|
- update: (ns: string, patch: { default?: string }) => {
|
|
|
- record('settings.update', { ns, patch })
|
|
|
- if (options.failSettings !== undefined) return remoteFail(options.failSettings)
|
|
|
- /* v8 ignore next -- the controller only ever sets `default` */
|
|
|
- defaultId.id = patch.default ?? defaultId.id
|
|
|
- return remoteOk({})
|
|
|
- },
|
|
|
openAgentPresetDirectory: (agentPreset: string) => {
|
|
|
record('openAgentPresetDirectory', { agentPreset })
|
|
|
if (options.failOpen !== undefined) return remoteFail(options.failOpen)
|
|
|
@@ -151,12 +173,16 @@ function harness(options: FakeOptions = {}) {
|
|
|
const defaultId = { id: 'standard' }
|
|
|
const calls: Recorded[] = []
|
|
|
let rosterChanges = 0
|
|
|
- const wired = { ...options, calls: options.calls ?? calls }
|
|
|
+ const pickerPolicy = options.pickerPolicy ?? { enabled: options.showPicker ?? true }
|
|
|
+ const wired = { ...options, pickerPolicy, calls: options.calls ?? calls }
|
|
|
const controller = new AgentPresetSectionController(
|
|
|
fakeCtx(presets, defaultId, wired),
|
|
|
() => { rosterChanges += 1 },
|
|
|
)
|
|
|
- return { controller, presets, defaultId, calls, rosterChanges: () => rosterChanges }
|
|
|
+ return {
|
|
|
+ controller, presets, defaultId, pickerPolicy, calls,
|
|
|
+ rosterChanges: () => rosterChanges,
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function copyOf(controller: AgentPresetSectionController): CopyDraft {
|
|
|
@@ -201,12 +227,12 @@ describe('loading the roster', () => {
|
|
|
expect(controller.store.getSnapshot().status).toBe('unavailable')
|
|
|
})
|
|
|
|
|
|
- it('keeps one load in flight rather than stacking reads', async () => {
|
|
|
+ it('coalesces concurrent refreshes into one follow-up read', async () => {
|
|
|
const { controller, calls } = harness()
|
|
|
|
|
|
- await Promise.all([controller.load(), controller.load()])
|
|
|
+ await Promise.all([controller.load(), controller.load(), controller.load()])
|
|
|
|
|
|
- expect(calls.filter(call => call.method === 'list')).toHaveLength(1)
|
|
|
+ expect(calls.filter(call => call.method === 'list')).toHaveLength(2)
|
|
|
})
|
|
|
|
|
|
it('surfaces a refusal as the page error', async () => {
|
|
|
@@ -528,14 +554,26 @@ describe('a controller with no roster listener', () => {
|
|
|
})
|
|
|
|
|
|
describe('the default preset', () => {
|
|
|
- it('writes the setting and re-reads the roster', async () => {
|
|
|
+ it('syncs the Host value that wins before the confirming read', async () => {
|
|
|
const { controller, defaultId } = harness()
|
|
|
+ const synced: string[] = []
|
|
|
+ const sync = (id: string): Promise<undefined> => {
|
|
|
+ synced.push(id)
|
|
|
+ return Promise.resolve(undefined)
|
|
|
+ }
|
|
|
await controller.load()
|
|
|
|
|
|
- await controller.makeDefault('mine')
|
|
|
+ const makingDefault = controller.makeDefault('mine', sync)
|
|
|
+ defaultId.id = 'standard'
|
|
|
+ await makingDefault
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot().rows.find(row => row.isDefault)?.id).toBe('standard')
|
|
|
+ expect(synced).toEqual(['standard'])
|
|
|
|
|
|
- expect(defaultId.id).toBe('mine')
|
|
|
- expect(controller.store.getSnapshot().rows.find(row => row.id === 'mine')?.isDefault).toBe(true)
|
|
|
+ const missingDefault = controller.makeDefault('mine', sync)
|
|
|
+ defaultId.id = 'missing'
|
|
|
+ await missingDefault
|
|
|
+ expect(synced).toEqual(['standard'])
|
|
|
})
|
|
|
|
|
|
it('surfaces a settings refusal as the page error', async () => {
|
|
|
@@ -546,4 +584,138 @@ describe('the default preset', () => {
|
|
|
|
|
|
expect(controller.store.getSnapshot().error).toContain('read-only settings')
|
|
|
})
|
|
|
+
|
|
|
+ it('keeps a composition sync failure on the page', async () => {
|
|
|
+ const { controller } = harness()
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.makeDefault(
|
|
|
+ 'mine',
|
|
|
+ () => Promise.resolve('blank session rejected the preset'),
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ error: 'blank session rejected the preset', policySaving: false,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('restores the policy lock after a thrown default write', async () => {
|
|
|
+ const { controller } = harness({ throwSettings: 'settings transport unavailable' })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.makeDefault('mine')
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ error: 'settings transport unavailable', policySaving: false,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+})
|
|
|
+
|
|
|
+describe('the new-session picker preference', () => {
|
|
|
+ it('ignores policy writes until the section is ready', async () => {
|
|
|
+ const { controller, calls } = harness({ showPicker: true })
|
|
|
+
|
|
|
+ await controller.setPickerVisible(false)
|
|
|
+
|
|
|
+ expect(calls.some(call => call.method === 'settings.update')).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('uses Standard while disabled and restores the saved default when re-enabled', async () => {
|
|
|
+ const { controller, calls, defaultId } = harness({
|
|
|
+ showPicker: true, failList: 'connection moved', failListAt: 2,
|
|
|
+ })
|
|
|
+ const synced: string[] = []
|
|
|
+ const sync = (id: string): Promise<undefined> => {
|
|
|
+ synced.push(id)
|
|
|
+ return Promise.resolve(undefined)
|
|
|
+ }
|
|
|
+ defaultId.id = 'mine'
|
|
|
+ await controller.load()
|
|
|
+ expect(controller.store.getSnapshot().rows.find(row => row.isDefault)?.id).toBe('mine')
|
|
|
+
|
|
|
+ await controller.setPickerVisible(false, sync)
|
|
|
+ expect(calls.filter(call => call.method === 'settings.update')[0]?.payload)
|
|
|
+ .toEqual({ ns: 'agent-presets', patch: { modeSelectionEnabled: false } })
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ showPicker: false, policySaving: false,
|
|
|
+ })
|
|
|
+ expect(controller.store.getSnapshot().rows.find(row => row.isDefault)?.id).toBe('standard')
|
|
|
+
|
|
|
+ await controller.setPickerVisible(true, sync)
|
|
|
+ expect(calls.filter(call => call.method === 'settings.update')[1]?.payload)
|
|
|
+ .toEqual({ ns: 'agent-presets', patch: { modeSelectionEnabled: true } })
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ showPicker: true, policySaving: false,
|
|
|
+ })
|
|
|
+ expect(controller.store.getSnapshot().rows.find(row => row.isDefault)?.id).toBe('mine')
|
|
|
+ expect(synced).toEqual(['standard', 'mine'])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('reloads Host truth and reports a refused visibility write', async () => {
|
|
|
+ const { controller } = harness({ showPicker: true, failSettings: 'read-only settings' })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.setPickerVisible(false)
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ showPicker: true, error: 'read-only settings', policySaving: false,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('reloads Host truth when another policy value wins the write', async () => {
|
|
|
+ const { controller } = harness({ showPicker: true, ignorePickerWrite: true })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.setPickerVisible(false)
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({ showPicker: true, policySaving: false })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('reloads a roster that cannot mark an effective default', async () => {
|
|
|
+ const { controller, defaultId } = harness({ showPicker: false })
|
|
|
+ defaultId.id = 'missing'
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.setPickerVisible(true)
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({ showPicker: true, policySaving: false })
|
|
|
+ expect(controller.store.getSnapshot().rows.every(row => !row.isDefault)).toBe(true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps a blank-task sync failure on the page', async () => {
|
|
|
+ const { controller } = harness({ showPicker: true })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.setPickerVisible(
|
|
|
+ false,
|
|
|
+ () => Promise.resolve('blank session rejected the policy'),
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ error: 'blank session rejected the policy', policySaving: false,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('reloads Host truth after a thrown visibility write', async () => {
|
|
|
+ const { controller } = harness({ showPicker: true, throwSettings: new Error('connection lost') })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.setPickerVisible(false)
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ showPicker: true, error: 'connection lost', policySaving: false,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('ignores default writes while mode selection is disabled', async () => {
|
|
|
+ const { controller, calls } = harness({ showPicker: false })
|
|
|
+ await controller.load()
|
|
|
+
|
|
|
+ await controller.makeDefault('mine')
|
|
|
+
|
|
|
+ expect(calls.some(call => call.method === 'settings.update')).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
})
|