lspz 0.11.7

AI-friendly LSP compression proxy
Documentation
# language: en
# capability: proxy
# purpose: 代理核心,管理 LSP 会话生命周期、消息路由和拦截器链执行。
# scope: src/proxy.rs, tests/

Feature: proxy

  @req:r21 @human
  Scenario: 生命周期管理
    - 系统 MUST 支持完整的 Proxy 生命周期:Created→Initializing→Ready→Working→ShuttingDown→Exited。

  @req:r31 @human
  Scenario: 消息路由
    - 系统 MUST 将 Client→Server 消息透明转发,Server→Client 消息经过拦截器链处理。

  @req:r40 @human
  Scenario: LSP 握手
    - 系统 MUST 正确处理 initialize/initialized 握手流程。

  @req:r49 @human
  Scenario: 配置驱动输出格式
    - 系统 MUST 使用 Config 控制运行时行为,包括 OutputFormat:passthrough 原样转发,toon 对通知与响应均输出 TOON 且响应保留 id,json 输出 compact JSON。

  @req:r56 @human
  Scenario: 优雅关闭
    - 系统 MUST 支持 shutdown 请求和 exit 通知的优雅关闭流程。

  @req:r63 @human
  Scenario: Cancel-safe 消息循环
    - 系统 MUST 在 select 竞争下以 cancel-safe 方式读取完整 LSP 帧,禁止中途取消导致字节丢失与帧错位。

  @req:r69 @human
  Scenario: 按 id 匹配握手与关闭响应
    - 系统 MUST 在 initialize 与 shutdown 流程中按 JSON-RPC id 匹配服务器响应,并将交错的通知转发给客户端。

  @req:r72 @human
  Scenario: Receive 超时
    - 系统 MUST 对 Proxy 侧等待服务器帧的 receive 使用有限超时(默认 30s),超时返回可观测错误而非无限阻塞。

  @req:r74 @human
  Scenario: pending_requests 回收
    - Proxy MUST 在收到 $/cancelRequest 时移除对应 pending id;MUST 按 TTL 剪枝超时未响应的 pending 条目,防止 HashMap 无限增长。

  @req:r24 @human
  Scenario: 空闲不因 receive 超时退出
    - Proxy message_loop MUST NOT 因服务器侧空闲超过 RECEIVE_TIMEOUT 而退出;超时仅用于握手或已发出请求后的等待。空闲 select! 中的 server receive MUST 无限期等待(或与客户端事件一并取消),禁止把空闲超时当成致命错误。

  @req:r21 @human
  Scenario: proxy-create
    - MUST hold: Given Config 实例; When Proxy::new(); Then 状态变为 Created.
    Given Config 实例
    When Proxy::new()
    Then 状态变为 Created

  @req:r21 @human
  Scenario: proxy-initialize
    - MUST hold: Given Created 状态的 Proxy; When 调用 initialize(); Then 状态变为 Initializing.
    Given Created 状态的 Proxy
    When 调用 initialize()
    Then 状态变为 Initializing

  @req:r31 @human
  Scenario: forward-client-msg
    - MUST hold: Given Client→Server 消息; When 消息循环; Then 透明转发给 LSP 服务器.
    Given Client→Server 消息
    When 消息循环
    Then 透明转发给 LSP 服务器

  @req:r31 @human
  Scenario: intercept-server-msg
    - MUST hold: Given Server→Client 消息; When 消息循环; Then 经过拦截器链处理后发送给客户端.
    Given Server→Client 消息
    When 消息循环
    Then 经过拦截器链处理后发送给客户端

  @req:r40 @human
  Scenario: initialize-handshake
    - MUST hold: Given LSP 客户端发送 initialize; When Proxy 处理; Then 转发给服务器并返回 capabilities.
    Given LSP 客户端发送 initialize
    When Proxy 处理
    Then 转发给服务器并返回 capabilities

  @req:r49 @human
  Scenario: passthrough-honored
    - MUST hold: Given output_format=passthrough; When 服务器发送 publishDiagnostics; Then 客户端收到未经压缩的原始 LSP JSON.
    Given output_format=passthrough
    When 服务器发送 publishDiagnostics
    Then 客户端收到未经压缩的原始 LSP JSON

  @req:r49 @human
  Scenario: toon-response-keeps-id
    - MUST hold: Given output_format=toon 且 pending completion 请求 id=7; When 服务器返回 completion 响应; Then 客户端收到带 id=7 且 result.format=toon 的 JSON-RPC 响应.
    Given output_format=toon 且 pending completion 请求 id=7
    When 服务器返回 completion 响应
    Then 客户端收到带 id=7 且 result.format=toon 的 JSON-RPC 响应

  @req:r56 @human
  Scenario: shutdown-flow
    - MUST hold: Given 调用 shutdown(); When Proxy 处理; Then 发送 shutdown 请求和 exit 通知.
    Given 调用 shutdown()
    When Proxy 处理
    Then 发送 shutdown 请求和 exit 通知

  @req:r63 @human
  Scenario: cancel-safe-loop
    - MUST hold: Given stdin 与 server 同时有不完整帧数据; When message_loop 运行 select; Then 任一侧只在完整帧到达后投递,另一侧未读字节保留在其 reader.
    Given stdin 与 server 同时有不完整帧数据
    When message_loop 运行 select
    Then 任一侧只在完整帧到达后投递,另一侧未读字节保留在其 reader

  @req:r69 @human
  Scenario: handshake-id-match
    - MUST hold: Given initialize 请求 id=1 且服务器先发 window/logMessage; When 执行握手; Then 通知先转发给客户端,随后转发 id=1 的 initialize 响应.
    Given initialize 请求 id=1 且服务器先发 window/logMessage
    When 执行握手
    Then 通知先转发给客户端,随后转发 id=1 的 initialize 响应

  @req:r72 @human
  Scenario: receive-timeout
    - MUST hold: Given 服务器不再发送数据; When 握手或消息循环等待 receive; Then 在超时后返回错误并记录日志.
    Given 服务器不再发送数据
    When 握手或消息循环等待 receive
    Then 在超时后返回错误并记录日志

  @req:r74 @human
  Scenario: cancel-prunes
    - MUST hold: Given pending 中有 id=7 的请求; When 客户端发送 $/cancelRequest id=7; Then pending_requests 不再包含 7.
    Given pending 中有 id=7 的请求
    When 客户端发送 $/cancelRequest id=7
    Then pending_requests 不再包含 7

  @req:r74 @human
  Scenario: ttl-prunes
    - MUST hold: Given pending 条目超过 TTL; When 跟踪新请求触发剪枝; Then 过期条目被移除.
    Given pending 条目超过 TTL
    When 跟踪新请求触发剪枝
    Then 过期条目被移除

  @req:r24 @human
  Scenario: idle-survives
    - MUST hold: Given Proxy Ready 且 30s 内无任何消息; When message_loop 继续运行; Then 不返回 Timeout 且进程保持 Ready.
    Given Proxy Ready 且 30s 内无任何消息
    When message_loop 继续运行
    Then 不返回 Timeout 且进程保持 Ready