diff --git a/AGENTS.md b/AGENTS.md index b8429c455f..038884d6bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,3 +2,4 @@ - 本地运行 `mix test` / `make -C elixir all` 时,应用会在测试前读取仓库的 `elixir/WORKFLOW.md` 并尝试启动 `server.port`(当前为 `40013`)。如果本机该端口已被占用,验证前临时把 `server.port` 改为 `null`(验证后恢复),或者在 BEAM 启动前把 `:workflow_file_path` 指向无服务端口的临时 workflow。 - 测试环境不要依赖宿主机 `GITHUB_PROJECT_OWNER` / `GITHUB_PROJECT_NUMBER` / `LINEAR_API_KEY` / `LINEAR_PROJECT_SLUG`;`elixir/test/support/test_support.exs` 已在每个测试前清理这些变量并在退出时恢复。 +- 当前 `gh` CLI 使用的 token 缺少 `read:org` scope;`gh pr view` / `gh pr edit` 这类走 GraphQL 的命令可能失败。遇到 PR 元数据更新或读取时,优先使用 REST `gh api repos///pulls|issues/...`,或使用会话内的 `github_graphql` 工具。 diff --git a/SPEC.md b/SPEC.md index 90f3cdb676..5773519205 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2635,3 +2635,63 @@ Required behavior: summary data is present. - Rollback is low risk: the change is isolated to the optional observability surface plus its snapshot contract. + +## 21. Issue 19 Coverage Remediation Plan + +### 21.1 Scope + +The CI gate currently fails because `make -C elixir coverage` reports `99.49%` total coverage while +the repository enforces a `100.00%` threshold. The only uncovered module is +`SymphonyElixir.Version`, whose current tests only exercise the Mix-generated charlist `:vsn` +shape (`~c"0.3.0"`) returned by `Application.spec(:symphony_elixir, :vsn)` in the test runtime. + +Required behavior: + +- Keep the runtime contract of `SymphonyElixir.Version.current/0`: it must always return a string. +- Normalize binary version metadata by returning it unchanged. +- Normalize charlist version metadata via `List.to_string/1`. +- Fall back to `"dev"` when the application metadata is missing or not a binary/charlist. +- Restore the coverage gate without lowering the threshold and without adding production logic that + exists only to satisfy tests. + +### 21.2 Affected Boundaries + +- `Types`: no new external domain types are required; the public version value remains a string. +- `Config`: no workflow or environment contract changes are expected. +- `Repo`: no tracker, filesystem, or network repository changes are required. +- `Service` / `Runtime`: refine `SymphonyElixir.Version` so raw BEAM application metadata is parsed + through one deterministic normalization boundary, while `SymphonyElixir.Codex.AppServer` + continues to consume only the normalized string. +- `UI`: no dashboard or HTTP surface changes are expected. +- `Tests`: add targeted unit coverage for the supported binary, charlist, and fallback metadata + shapes plus one smoke assertion that the zero-arity runtime entrypoint still returns a string. + +### 21.3 Milestones + +1. Isolate the raw-version normalization path in `elixir/lib/symphony_elixir/version.ex` so every + supported metadata shape can be exercised deterministically in tests without mutating global app + state ad hoc. +2. Add focused tests under `elixir/test/symphony_elixir/` that cover binary metadata, charlist + metadata, unsupported/nil metadata, and the existing runtime entrypoint. +3. Re-run coverage and the full Elixir gate to prove the module reaches `100.00%` coverage and the + repository-level CI command passes unchanged. + +### 21.4 Test Plan + +- During spec review, run `cd elixir && mix specs.check` after any edits to this specification. +- After implementation, run `cd elixir && mix test test/symphony_elixir/version_test.exs` to verify + every supported version-metadata shape. +- After implementation, run `make -C elixir coverage` to confirm the strict `100.00%` coverage gate. +- After implementation, run `make -C elixir all` to verify the full local CI path still passes. + +### 21.5 Compatibility, Risks, and Rollback + +- `SymphonyElixir.Codex.AppServer` and any future callers must continue receiving a plain string; + there is no acceptable behavior change at that call site. +- The implementation must avoid scattering type checks across callers; raw `:vsn` parsing belongs + in `SymphonyElixir.Version` as the single boundary for this metadata. +- The main risk is introducing a test seam that leaks unnecessary API surface. If an auxiliary + function is required for deterministic tests, keep it narrowly scoped and aligned with the real + runtime parsing boundary. +- Rollback is low risk: revert the `Version` refactor and its focused tests without touching + workflow, tracker, or release contracts. diff --git a/elixir/lib/symphony_elixir/version.ex b/elixir/lib/symphony_elixir/version.ex index f281dde98d..4862b6f6fc 100644 --- a/elixir/lib/symphony_elixir/version.ex +++ b/elixir/lib/symphony_elixir/version.ex @@ -5,9 +5,17 @@ defmodule SymphonyElixir.Version do @spec current() :: String.t() def current do - case Application.spec(:symphony_elixir, :vsn) do - version when is_binary(version) -> version - version when is_list(version) -> List.to_string(version) + :symphony_elixir + |> Application.spec(:vsn) + |> normalize() + end + + @doc false + @spec normalize(term()) :: String.t() + def normalize(version) do + case version do + value when is_binary(value) -> value + value when is_list(value) -> List.to_string(value) _ -> "dev" end end diff --git a/elixir/test/symphony_elixir/version_test.exs b/elixir/test/symphony_elixir/version_test.exs new file mode 100644 index 0000000000..82f7c3e1fc --- /dev/null +++ b/elixir/test/symphony_elixir/version_test.exs @@ -0,0 +1,26 @@ +defmodule SymphonyElixir.VersionTest do + use ExUnit.Case, async: true + + alias SymphonyElixir.Version + + describe "normalize/1" do + test "returns binaries unchanged" do + assert Version.normalize("0.3.0") == "0.3.0" + end + + test "converts charlists to strings" do + assert Version.normalize(~c"0.3.0") == "0.3.0" + end + + test "falls back to dev for unsupported metadata" do + assert Version.normalize(nil) == "dev" + end + end + + test "current/0 returns a normalized string" do + version = Version.current() + + assert is_binary(version) + assert version != "" + end +end