fix: restore the credential-rejected branch and admit SRC absence by key

Review follow-ups that are logic rather than documentation:

- rpc.schema.ts had lost the credential-rejected branch while api-proxy.ts
  still returns that code, so a legitimate business error failed the
  client's response parse. Restore the branch and assert it.
- rpc-schemas.spec.ts had lost the workspace list, archiveSession and
  insertSessionBefore cases along with the command schemas; those routes
  still ship, so restore their coverage.
- An omitted SRC field is now recognized by an absent key instead of an
  undefined value, which makes the allowance assertExactArguments already
  granted reachable; an explicitly undefined field stays invalid input. A
  weak descriptor's undefined result rides the wire as an absent value,
  matching the envelope removal.
- The chooser unmounts an already-created backend when the surface entry
  fails to load, and no longer reverses the captured id array in place.
This commit is contained in:
imccyu
2026-08-11 23:01:20 +08:00
parent 378141bf5d
commit 9b2925e50f
6 changed files with 79 additions and 7 deletions

View File

@@ -72,11 +72,8 @@ export async function apply(ctx: Context): Promise<void> {
// backend lands first: the surface's browser half drives the capability
// the backend registers.
const ids: string[] = []
for (const name of [BACKEND_PACKAGES[backend], SURFACE_PACKAGES[backend]]) {
ids.push(await ctx.loader.create({ name }))
}
return async () => {
for (const id of ids.reverse()) {
const unmount = async () => {
for (const id of [...ids].reverse()) {
// Tree teardown (group.stop) can have removed the entry already;
// nothing is left to unmount or await then.
if (ctx.loader.store[id] === undefined) continue
@@ -85,5 +82,17 @@ export async function apply(ctx: Context): Promise<void> {
await ctx.loader.remove(id)
}
}
try {
for (const name of [BACKEND_PACKAGES[backend], SURFACE_PACKAGES[backend]]) {
ids.push(await ctx.loader.create({ name }))
}
} catch (cause) {
// Setup owns the entries it created until it returns the disposer: leaving
// the backend mounted would make a retry collide with its own
// directoryPicker registration.
await unmount()
throw cause
}
return unmount
}, 'directory-picker-auto: interaction entries')
}

View File

@@ -88,7 +88,10 @@ afterEach(async () => {
})
/** Write a two-row cordis.yml (webserver + chooser), then boot it through the real Loader. */
async function loadComposition(bindHost: '127.0.0.1' | '0.0.0.0'): Promise<{ ctx: Context; configPath: string }> {
async function loadComposition(
bindHost: '127.0.0.1' | '0.0.0.0',
options: { failSurface?: boolean } = {},
): Promise<{ ctx: Context; configPath: string }> {
root = await mkdtemp(join(tmpdir(), 'dsh-directory-picker-auto-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
@@ -115,6 +118,9 @@ async function loadComposition(bindHost: '127.0.0.1' | '0.0.0.0'): Promise<{ ctx
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (options.failSurface === true && (specifier === NATIVE_SURFACE || specifier === BROWSE_SURFACE)) {
throw new Error(`surface import failed: ${specifier}`)
}
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
return modules.get(specifier)
},
@@ -209,6 +215,17 @@ describe('real Loader composition', () => {
expect(entryNames(ctx)).not.toContain(NATIVE_SURFACE)
})
it('unmounts the backend when the surface entry fails to load', { timeout: 60_000 }, async () => {
stubAttendedHost()
await expect(loadComposition('127.0.0.1', { failSurface: true })).rejects.toThrow(/surface import failed/)
// Setup owns both entries until it returns its disposer, so a failed surface
// must take the mounted backend with it: otherwise a retry collides with the
// directoryPicker registration this backend already made.
expect(entryNames(context!)).not.toContain(NATIVE)
expect(context!.get('directoryPicker')).toBeUndefined()
})
it('tolerates the mounted entry being removed by the tree before the chooser unloads', { timeout: 60_000 }, async () => {
stubAttendedHost()
const { ctx, configPath } = await loadComposition('127.0.0.1')