Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<owner>/<repo>/pulls|issues/...`,或使用会话内的 `github_graphql` 工具。
60 changes: 60 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 11 additions & 3 deletions elixir/lib/symphony_elixir/version.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions elixir/test/symphony_elixir/version_test.exs
Original file line number Diff line number Diff line change
@@ -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