# Config restart capability flip

```yaml qa-scenario
id: config-restart-capability-flip
title: Config restart capability flip
surface: config
objective: Verify a restart-triggering config change flips capability inventory and the same session successfully uses the newly restored tool after wake-up.
successCriteria:
  - Capability is absent before the restart-triggering patch.
  - Restart sentinel wakes the same session back up after config patch.
  - The restored capability appears in tools.effective and works in the follow-up turn.
docsRefs:
  - docs/gateway/configuration.md
  - docs/gateway/protocol.md
  - docs/tools/image-generation.md
codeRefs:
  - src/gateway/server-methods/config.ts
  - src/gateway/server-restart-sentinel.ts
  - src/gateway/server-methods/tools-effective.ts
  - extensions/qa-lab/src/suite.ts
execution:
  kind: flow
  summary: Verify a restart-triggering config change flips capability inventory and the same session successfully uses the newly restored tool after wake-up.
  config:
    setupPrompt: "Capability flip setup: acknowledge this setup so restart wake-up has a route."
    imagePrompt: "Capability flip image check: generate a QA lighthouse image in this turn right now. Do not acknowledge first, do not promise future work, and do not stop before using image_generate. Final reply must include the MEDIA path."
    imagePromptSnippet: "Capability flip image check"
    deniedTool: image_generate
```

```yaml qa-flow
steps:
  - name: restores image_generate after restart and uses it in the same session
    actions:
      - call: ensureImageGenerationConfigured
        args:
          - ref: env
      - call: readConfigSnapshot
        saveAs: original
        args:
          - ref: env
      - set: originalTools
        value:
          expr: "original.config.tools && typeof original.config.tools === 'object' ? original.config.tools : null"
      - set: originalToolsDeny
        value:
          expr: "originalTools ? (Object.prototype.hasOwnProperty.call(originalTools, 'deny') ? structuredClone(originalTools.deny) : undefined) : undefined"
      - set: denied
        value:
          expr: "Array.isArray(originalToolsDeny) ? originalToolsDeny.map((entry) => String(entry)) : []"
      - set: deniedWithImage
        value:
          expr: "denied.includes(config.deniedTool) ? denied : [...denied, config.deniedTool]"
      - set: sessionKey
        value: agent:qa:capability-flip
      - call: createSession
        args:
          - ref: env
          - Capability flip
          - ref: sessionKey
      - try:
          actions:
            - call: patchConfig
              args:
                - env:
                    ref: env
                  patch:
                    tools:
                      deny:
                        ref: deniedWithImage
            - call: waitForGatewayHealthy
              args:
                - ref: env
            - call: waitForQaChannelReady
              args:
                - ref: env
                - 60000
            - call: runAgentPrompt
              args:
                - ref: env
                - sessionKey:
                    ref: sessionKey
                  message:
                    expr: config.setupPrompt
                  timeoutMs:
                    expr: liveTurnTimeoutMs(env, 30000)
            - call: readEffectiveTools
              saveAs: beforeTools
              args:
                - ref: env
                - ref: sessionKey
            - assert:
                expr: "!beforeTools.has(config.deniedTool)"
                message:
                  expr: "`${config.deniedTool} still present before capability flip`"
            - set: wakeMarker
              value:
                expr: "`QA-CAPABILITY-${randomUUID().slice(0, 8)}`"
            - call: patchConfig
              args:
                - env:
                    ref: env
                  patch:
                    tools:
                      deny:
                        expr: "originalToolsDeny === undefined ? null : originalToolsDeny"
                    agents:
                      defaults:
                        imageGenerationModel:
                          primary: openai/gpt-image-1
                  sessionKey:
                    ref: sessionKey
                  note:
                    ref: wakeMarker
            - call: waitForGatewayHealthy
              args:
                - ref: env
                - 60000
            - call: waitForQaChannelReady
              args:
                - ref: env
                - 60000
            - call: waitForCondition
              saveAs: afterTools
              args:
                - lambda:
                    async: true
                    expr: "(() => readEffectiveTools(env, sessionKey).then((tools) => (tools.has('image_generate') ? tools : undefined)))()"
                - expr: liveTurnTimeoutMs(env, 45000)
                - 500
            - set: imageStartedAtMs
              value:
                expr: "Date.now()"
            - call: runAgentPrompt
              args:
                - ref: env
                - sessionKey:
                    ref: sessionKey
                  message:
                    expr: config.imagePrompt
                  timeoutMs:
                    expr: liveTurnTimeoutMs(env, 45000)
            - call: resolveGeneratedImagePath
              saveAs: mediaPath
              args:
                - env:
                    ref: env
                  promptSnippet:
                    expr: config.imagePromptSnippet
                  startedAtMs:
                    ref: imageStartedAtMs
                  timeoutMs:
                    expr: liveTurnTimeoutMs(env, 45000)
            # Tool-call assertion (criterion 2 of the parity completion
            # gate in #64227): the restored `image_generate` capability
            # must have actually fired as a real tool call. Without this
            # assertion, a prose reply that just mentions a MEDIA path
            # could satisfy the scenario, so strengthen it by requiring
            # the mock to have recorded `plannedToolName: "image_generate"`
            # against a post-restart request. The `!env.mock || ...`
            # guard means this check only runs in mock mode (where
            # `/debug/requests` is available); live-frontier runs skip
            # it and still pass the rest of the scenario.
            - assert:
                expr: "!env.mock || [...(await fetchJson(`${env.mock.baseUrl}/debug/requests`))].some((request) => String(request.allInputText ?? '').toLowerCase().includes('capability flip image check') && request.plannedToolName === 'image_generate')"
                message:
                  expr: "`expected image_generate tool call during capability flip scenario, saw plannedToolNames=${JSON.stringify([...(await fetchJson(`${env.mock.baseUrl}/debug/requests`))].filter((request) => String(request.allInputText ?? '').toLowerCase().includes('capability flip image check')).map((request) => request.plannedToolName ?? null))}`"
          finally:
            - call: patchConfig
              args:
                - env:
                    ref: env
                  patch:
                    tools:
                      deny:
                        expr: "originalToolsDeny === undefined ? null : originalToolsDeny"
            - call: waitForGatewayHealthy
              args:
                - ref: env
            - call: waitForQaChannelReady
              args:
                - ref: env
                - 60000
    detailsExpr: "`${wakeMarker}\\n${config.deniedTool}=${String(afterTools.has(config.deniedTool))}\\nMEDIA:${mediaPath}`"
```
