From e4e6638319e38c7536fee8063ac2e1ccf3f96a56 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 17:03:30 +0500 Subject: [PATCH 1/5] fix(module): coverage over nothing is not coverage `gds module coverage` reported success having compared no module against any gate. An estate whose repositories are not declared as git-submodule-consumer relationships gets `"modules": []` and `result: succeeded`, which is indistinguishable from every module being covered. The estates most likely to hit it are exactly the ones asking whether their gates are watched. setup-systems ran it against eight repositories expecting a verdict on fourteen required contexts each, and got green over an empty set. That is the same class of defect this command exists to find, arriving inside the tool meant to find it. Empty scope now emits GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN, carrying the declared relationship count so the reason is legible. Not-proven rather than validation: an estate with no modules is not invalid, but nothing about its gates was proven either. There were no tests for CoverModules at all, which is part of why this shipped. The new test runs the real command against this repository, which declares no relationships, and asserts the specific finding code rather than a non-zero exit -- with `XDG_CONFIG_HOME` redirected the command fails earlier at GDS_POLICY_ESTATE_NOT_PROVEN and never reaches this path, so a test asserting only the exit code would have passed without covering anything. Verified by mutation: replacing the guard condition with `false` makes it fail. --- core/app/module_coverage.go | 17 +++++++++++++++++ core/cli/root_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/core/app/module_coverage.go b/core/app/module_coverage.go index 4f64246..419fa5a 100644 --- a/core/app/module_coverage.go +++ b/core/app/module_coverage.go @@ -127,6 +127,23 @@ func (services *Services) CoverModules( Evidence: map[string]any{"module": selected}, }) } + // Coverage over nothing is not coverage. Without this the command reports + // success having compared no module against any gate, which is + // indistinguishable from every module being covered -- and the estates most + // likely to hit it are the ones whose repositories were never declared as + // modules, which is exactly when someone is asking whether their gates are + // watched. + if len(data.Modules) == 0 { + findings = append(findings, domain.Finding{ + Code: "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN", Severity: domain.SeverityHigh, + Message: "No declared git-submodule-consumer relationship was covered, " + + "so no gate was compared.", + Evidence: map[string]any{ + "declared_relationships": len(consumer.Relationships), + "repository_id": consumer.Repository.ID, + }, + }) + } return domain.NewEnvelope(command, classifyFindings(findings), data, findings...) } diff --git a/core/cli/root_test.go b/core/cli/root_test.go index 8d0c680..832759e 100644 --- a/core/cli/root_test.go +++ b/core/cli/root_test.go @@ -1127,3 +1127,29 @@ func repositoryGitIndexPath(t *testing.T, root string) string { t.Helper() return runSessionGit(t, root, "rev-parse", "--path-format=absolute", "--git-path", "index") } + +// TestModuleCoverageOverNothingIsNotSuccess covers the case the command was +// silent about: this repository declares no git-submodule-consumer +// relationship, so coverage compares no module against any gate. Reporting +// success there is indistinguishable from every module being covered, and the +// estates most likely to hit it are the ones asking whether their gates are +// watched at all. +func TestModuleCoverageOverNothingIsNotSuccess(t *testing.T) { + root := repositoryRoot(t) + exitCode, envelope, stderr := executeJSON(t, + "--json", "--cwd", root, "module", "coverage", + ) + if exitCode == 0 { + t.Fatalf("coverage over an empty module set exited 0; envelope = %#v, stderr = %q", + envelope, stderr) + } + found := false + for _, finding := range envelope.Findings { + if finding.Code == "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN" { + found = true + } + } + if !found { + t.Fatalf("no GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN finding; envelope = %#v", envelope) + } +} From 7566d3f49e2f9a0c987a9218a06009e15cc2591f Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 17:04:04 +0500 Subject: [PATCH 2/5] chore(projections): regenerate onto the current source tree `core/app` is one of the source paths the bundle lock digests, so the coverage fix changed the source tree identity and left both projections stale against it. Regenerated through `gds generate repository` plan/apply; `--check` now reports no findings. --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index 0fbe5de..33be273 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:017d389794f2e9f7bba28e18cf4dd61c51ed56eef6eb2600e4c8f8b919bce854" - digest: "sha256:9eb5f71d3114d6e5364d996075e8ab12703223c9c5f0bab02af42eef4e9b7b80" + source_tree_digest: "sha256:3e54c5be47c82e080ac3a25275431df6f2f144dc86b9c2e8e45bd2bb189a3fe6" + digest: "sha256:90d20dcbf26715c467fd39b91a2203bd48b7cfe306250433d49143a058fc39f4" projection: - input_digest: "sha256:cc7b9a4f7c11ad8b00708e56ea2afbe3a33f65f0295baad7ef98b636cb9c16ef" - output_digest: "sha256:a241cbac1606ef94318d54a078c5ebcc6dcc69dbf828676b9dc36f0914016958" + input_digest: "sha256:a049cd9a8822b2b51a01e08a632f7495312de2a8bbb0d7dcabf175b783156f4f" + output_digest: "sha256:8b0e5600470df0d9735cf1f56daaec46b22df1f14964e98ad7698e1a0acf942c" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:c77919497087a60415fed43024d50a3a0d5c69d4cb5b4ed324db443c772ccf93" + digest: "sha256:f7cf45d1b4016639fda4e411f307347d79e7ff63fabec6806c37816dd2591ce3" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index f47b44c..02f39f4 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:017d389794f2e9f7bba28e18cf4dd61c51ed56eef6eb2600e4c8f8b919bce854 -# input-digest: sha256:cc7b9a4f7c11ad8b00708e56ea2afbe3a33f65f0295baad7ef98b636cb9c16ef +# source-tree-digest: sha256:3e54c5be47c82e080ac3a25275431df6f2f144dc86b9c2e8e45bd2bb189a3fe6 +# input-digest: sha256:a049cd9a8822b2b51a01e08a632f7495312de2a8bbb0d7dcabf175b783156f4f # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml From 9c06c2be0d52e7c9da6d1dabb841a0f2e6a10f9a Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 17:07:52 +0500 Subject: [PATCH 3/5] chore(projections): regenerate onto the merged source tree The merge with main brought in the schema-digest change's own regenerated projections; both are generated from the same source paths, so the merged tree needs one regeneration rather than either side's. `--check` reports no findings. --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index d41ba25..2f608ff 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:a18f15d00b467f9585fecf7ee3230f00025f69726e17135f14eb99f3036b5827" - digest: "sha256:7d370fff3455208269852d3bd0f7e4eff1e61b3eb8e80df9c967245bf464cda5" + source_tree_digest: "sha256:6575f7ab7d654653e39aa0d9eb5648c2756f4f8ae509f008c057ee86a9c2adb7" + digest: "sha256:42bc2599a495e89bd447962dfeeed5dcdb678050b392d311a158fafe27c2ad5e" projection: - input_digest: "sha256:88f5073a543617f2b37990db826b8777ef8c396eb658de403f60e2e480e15f62" - output_digest: "sha256:d76ab0b017214dce592c2ae8b23e30b89caabf5a0197be3b25dae6d948cd00aa" + input_digest: "sha256:79d3ea8ed38ee3b2619c9d07194a4f9a53f729d27cdd6863777b806857f7e310" + output_digest: "sha256:dd5ead67cc14484b9bd0f47467474cc21dd1f30c22146df0e7982bec006e9fcf" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:1508f13759d4a238114e35d7c6ee6e0f6d2b3b09e18d8fb7eff46b534143cb73" + digest: "sha256:7e1fecd895583d8133905ec4a1ef7052e9752ee194f9040dc07c6e83f51f6f2e" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index b25364c..86f9549 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:a18f15d00b467f9585fecf7ee3230f00025f69726e17135f14eb99f3036b5827 -# input-digest: sha256:88f5073a543617f2b37990db826b8777ef8c396eb658de403f60e2e480e15f62 +# source-tree-digest: sha256:6575f7ab7d654653e39aa0d9eb5648c2756f4f8ae509f008c057ee86a9c2adb7 +# input-digest: sha256:79d3ea8ed38ee3b2619c9d07194a4f9a53f729d27cdd6863777b806857f7e310 # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml From 64dce4e6d33a1b7e40c1f24263fd69312cef97f1 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 17:16:19 +0500 Subject: [PATCH 4/5] test(module): cover the empty scope where the estate is a fixture The first version of this test lived in core/cli and was green on my machine for the wrong reason: a registered estate happened to be configured there, so the command reached the guard. CI has none, so `gds module coverage` failed earlier at GDS_POLICY_ESTATE_NOT_PROVEN and the test never touched the path it claimed to cover. That is the defect this pull request fixes, reproduced inside its own test. It was only visible because the assertion named the finding code; an assertion on a non-zero exit would have stayed green over the wrong refusal, locally and in CI alike. Moved to core/app against `appTestRepositoryRoot`, which builds an external estate in a temporary directory and exports GDS_ESTATE_ROOT, so the test carries its own preconditions. Verified twice: mutating the guard to `false` makes it fail, and it passes with HOME, XDG_CONFIG_HOME, XDG_STATE_HOME and GDS_ESTATE_ROOT all cleared, which is the CI shape. --- core/app/module_coverage_test.go | 44 ++++++++++++++++++++++++++++++++ core/cli/root_test.go | 26 ------------------- 2 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 core/app/module_coverage_test.go diff --git a/core/app/module_coverage_test.go b/core/app/module_coverage_test.go new file mode 100644 index 0000000..75f75a5 --- /dev/null +++ b/core/app/module_coverage_test.go @@ -0,0 +1,44 @@ +package app + +import ( + "context" + "testing" + + "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" +) + +// TestModuleCoverageOverNothingIsNotProven covers the case the command was +// silent about. An estate whose repositories are not declared as +// git-submodule-consumer relationships gets an empty module set, and reporting +// success there is indistinguishable from every module being covered -- while +// the estates most likely to hit it are exactly the ones asking whether their +// gates are watched at all. +// +// This runs against the external estate fixture rather than the developer's own +// machine. A first version of this test lived in core/cli and passed locally +// only because a registered estate happened to be configured there; in CI the +// command failed earlier at GDS_POLICY_ESTATE_NOT_PROVEN and never reached this +// path, so it was green over the wrong refusal. +func TestModuleCoverageOverNothingIsNotProven(t *testing.T) { + root := appTestRepositoryRoot(t) + runtimePath := appTestRuntimeConfig(t, root) + services, err := NewServices(DefaultClock) + if err != nil { + t.Fatal(err) + } + envelope := services.CoverModules(context.Background(), root, ModuleCoverageOptions{ + GitHubReadOptions: GitHubReadOptions{RuntimeConfig: runtimePath}, + }) + if envelope.ExitClass == domain.ExitSuccess { + t.Fatalf("coverage over an empty module set reported success: %#v", envelope) + } + found := false + for _, finding := range envelope.Findings { + if finding.Code == "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN" { + found = true + } + } + if !found { + t.Fatalf("no GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN finding: %#v", envelope) + } +} diff --git a/core/cli/root_test.go b/core/cli/root_test.go index 832759e..8d0c680 100644 --- a/core/cli/root_test.go +++ b/core/cli/root_test.go @@ -1127,29 +1127,3 @@ func repositoryGitIndexPath(t *testing.T, root string) string { t.Helper() return runSessionGit(t, root, "rev-parse", "--path-format=absolute", "--git-path", "index") } - -// TestModuleCoverageOverNothingIsNotSuccess covers the case the command was -// silent about: this repository declares no git-submodule-consumer -// relationship, so coverage compares no module against any gate. Reporting -// success there is indistinguishable from every module being covered, and the -// estates most likely to hit it are the ones asking whether their gates are -// watched at all. -func TestModuleCoverageOverNothingIsNotSuccess(t *testing.T) { - root := repositoryRoot(t) - exitCode, envelope, stderr := executeJSON(t, - "--json", "--cwd", root, "module", "coverage", - ) - if exitCode == 0 { - t.Fatalf("coverage over an empty module set exited 0; envelope = %#v, stderr = %q", - envelope, stderr) - } - found := false - for _, finding := range envelope.Findings { - if finding.Code == "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN" { - found = true - } - } - if !found { - t.Fatalf("no GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN finding; envelope = %#v", envelope) - } -} From b205bfb0b99045ddd7ccec415fe9ca74c7eaee43 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 31 Aug 2026 17:20:19 +0500 Subject: [PATCH 5/5] chore(projections): regenerate onto the current source tree `core/app` is a digested source path in whole, so a test-only change to it moves the source tree identity as much as a change to the command does. --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index 2f608ff..a8dcd8e 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:6575f7ab7d654653e39aa0d9eb5648c2756f4f8ae509f008c057ee86a9c2adb7" - digest: "sha256:42bc2599a495e89bd447962dfeeed5dcdb678050b392d311a158fafe27c2ad5e" + source_tree_digest: "sha256:66532e92cee2ba8351911491ea4d8267fe853db10ebe36559551cb21aad5f209" + digest: "sha256:02032875c4b1e479d953ba4e9b766cf260bc469a81c38856ab6a6c6044b1c147" projection: - input_digest: "sha256:79d3ea8ed38ee3b2619c9d07194a4f9a53f729d27cdd6863777b806857f7e310" - output_digest: "sha256:dd5ead67cc14484b9bd0f47467474cc21dd1f30c22146df0e7982bec006e9fcf" + input_digest: "sha256:02a7ff040caee7de0f5de1b2402485a7e6972b68e867413ad7a1d71add5d0336" + output_digest: "sha256:8fd7fc9e1adc79eec4876daaa1d9711882d3959584c92fc5c427c64ee6ecebce" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:7e1fecd895583d8133905ec4a1ef7052e9752ee194f9040dc07c6e83f51f6f2e" + digest: "sha256:184bc5f09a65158b07fe4d6948c4d805869ddb35f9dcfaf528c5f5b4720abebb" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 86f9549..5aafd3a 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:6575f7ab7d654653e39aa0d9eb5648c2756f4f8ae509f008c057ee86a9c2adb7 -# input-digest: sha256:79d3ea8ed38ee3b2619c9d07194a4f9a53f729d27cdd6863777b806857f7e310 +# source-tree-digest: sha256:66532e92cee2ba8351911491ea4d8267fe853db10ebe36559551cb21aad5f209 +# input-digest: sha256:02a7ff040caee7de0f5de1b2402485a7e6972b68e867413ad7a1d71add5d0336 # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml