cem_acpt 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.worktreeinclude +1 -0
  4. data/CLAUDE.md +64 -25
  5. data/Gemfile.lock +1 -1
  6. data/README.md +20 -7
  7. data/docs/ARCHITECTURE.md +1042 -0
  8. data/docs/rfcs/0000-template.md +54 -0
  9. data/docs/rfcs/0001-fix-bolt-missing-skip-path.md +105 -0
  10. data/docs/rfcs/0002-fix-default-character-substitutions.md +119 -0
  11. data/docs/rfcs/0003-windows-image-builder-template.md +110 -0
  12. data/docs/rfcs/0004-image-name-truncation-off-by-one.md +108 -0
  13. data/docs/rfcs/0005-os-dispatch-replace-windows-heuristic.md +117 -0
  14. data/docs/rfcs/0006-configurable-windows-bucket.md +96 -0
  15. data/docs/rfcs/0007-logging-quiet-and-typos.md +121 -0
  16. data/docs/rfcs/0008-namespace-platform-classes.md +110 -0
  17. data/docs/rfcs/0009-bolt-log-formatter-cleanup.md +111 -0
  18. data/docs/rfcs/0010-dead-code-cleanup.md +83 -0
  19. data/docs/rfcs/0011-provisioner-factory-consistency.md +89 -0
  20. data/docs/rfcs/README.md +34 -0
  21. data/lib/cem_acpt/cli.rb +10 -1
  22. data/lib/cem_acpt/config/cem_acpt.rb +4 -1
  23. data/lib/cem_acpt/image_builder/errors.rb +24 -0
  24. data/lib/cem_acpt/image_builder/provision_commands.rb +15 -3
  25. data/lib/cem_acpt/image_builder.rb +29 -2
  26. data/lib/cem_acpt/image_name_builder.rb +8 -1
  27. data/lib/cem_acpt/platform/gcp.rb +112 -106
  28. data/lib/cem_acpt/platform.rb +21 -19
  29. data/lib/cem_acpt/provision/terraform/linux.rb +1 -1
  30. data/lib/cem_acpt/provision/terraform/os_data.rb +23 -0
  31. data/lib/cem_acpt/provision/terraform/windows.rb +7 -1
  32. data/lib/cem_acpt/provision/terraform.rb +20 -16
  33. data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +2 -1
  34. data/lib/cem_acpt/test_runner/log_formatter.rb +0 -1
  35. data/lib/cem_acpt/test_runner.rb +21 -8
  36. data/lib/cem_acpt/utils/winrm_runner.rb +4 -3
  37. data/lib/cem_acpt/utils.rb +0 -12
  38. data/lib/cem_acpt/version.rb +1 -1
  39. data/lib/cem_acpt.rb +19 -7
  40. data/lib/terraform/gcp/linux/main.tf +6 -1
  41. data/lib/terraform/image/gcp/linux/main.tf +8 -1
  42. data/specifications/CEM-6713.md +165 -0
  43. data/specifications/CEM-6714.md +271 -0
  44. data/specifications/CEM-6715.md +133 -0
  45. data/specifications/CEM-6716.md +160 -0
  46. data/specifications/CEM-6717.md +239 -0
  47. data/specifications/CEM-6718.md +120 -0
  48. data/specifications/CEM-6719.md +173 -0
  49. metadata +26 -11
  50. data/.claude/settings.local.json +0 -7
  51. data/lib/cem_acpt/action_result.rb +0 -91
  52. data/lib/cem_acpt/puppet_helpers.rb +0 -38
  53. data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +0 -65
  54. data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +0 -54
@@ -0,0 +1,133 @@
1
+ # CEM-6715 — Make the Windows GCS bucket configurable
2
+
3
+ ## Summary
4
+
5
+ Replace the hard-coded `gs://win_cem_acpt` bucket in the Windows test
6
+ path with a configurable value (`platform.gcp.windows_bucket`),
7
+ defaulting to `win_cem_acpt` so internal puppetlabs users see no
8
+ behavior change. Surface a `--windows-bucket` CLI override.
9
+
10
+ Implements RFC 0006 (`docs/rfcs/0006-configurable-windows-bucket.md`).
11
+
12
+ ## Functional Behavior
13
+
14
+ ### Config schema
15
+ - New nested key `platform.gcp.windows_bucket`.
16
+ - Default: `'win_cem_acpt'` (preserves current behavior).
17
+ - Read via the existing `config.get('platform.gcp.windows_bucket')`
18
+ dot-notation API.
19
+
20
+ ### CLI flag
21
+ - New `--windows-bucket BUCKET` long flag (no short alias) on the
22
+ `cem_acpt` command (not `cem_acpt_image` — image-build path does not
23
+ use the bucket).
24
+ - Sets `options[:platform][:gcp][:windows_bucket] = BUCKET`, which
25
+ merges into config above the user config / config-file layer.
26
+
27
+ ### Runtime use in `test_runner.rb`
28
+ - New private helper `windows_bucket_uri` that returns
29
+ `"gs://#{config.get('platform.gcp.windows_bucket')}"` and raises a
30
+ clear error if the configured value is `nil` or empty.
31
+ - `upload_module_to_bucket` (line 386) uses `windows_bucket_uri` instead
32
+ of the literal `'gs://win_cem_acpt'`.
33
+ - `cleanup_bucket` continues to operate on `@run_data[:win_remote_module_path]`
34
+ (unchanged — already derived from upload).
35
+ - `WinNode` (in `lib/cem_acpt/utils/winrm_runner.rb`) is updated to
36
+ accept the bucket URI and use it on its own hardcoded line 83
37
+ (`gcloud storage cp gs://win_cem_acpt/#{@mod_name} ...`). Without this
38
+ fix, the RFC's stated goal is not actually achieved.
39
+
40
+ ## Input/Output Contracts
41
+
42
+ ### `CemAcpt::TestRunner::Runner#windows_bucket_uri` (private)
43
+ - Input: none (reads from `config`)
44
+ - Output: `String` of the form `"gs://<bucket>"`
45
+ - Raises: `RuntimeError` (message: `'platform.gcp.windows_bucket is not configured'`)
46
+ when the config value is nil or an empty string.
47
+
48
+ ### `CemAcpt::Utils::WinRMRunner::WinNode.new`
49
+ - New signature: `initialize(login_info, mod_name, bucket_uri)`
50
+ - `bucket_uri` is the full URI string, e.g. `'gs://win_cem_acpt'`,
51
+ passed in from the runner. The class no longer hard-codes the bucket.
52
+
53
+ ### CLI
54
+ - `--windows-bucket BUCKET` writes to nested options as
55
+ `{ platform: { gcp: { windows_bucket: BUCKET } } }`.
56
+
57
+ ## Edge Cases
58
+
59
+ - **Empty / nil bucket value (e.g. user sets `windows_bucket: ''` in
60
+ YAML).** `windows_bucket_uri` raises with a clear message before any
61
+ `gcloud` call is attempted.
62
+ - **Bucket value with a `gs://` prefix** (e.g. user pastes a URL).
63
+ Out of scope. We expect a plain bucket name. Document the expectation
64
+ in the README and rely on `gcloud` to fail loudly if a malformed URI
65
+ is passed. (Avoiding silent normalization keeps the contract simple.)
66
+ - **Linux-only test runs.** Neither `upload_module_to_bucket` nor
67
+ `cleanup_bucket` runs (gated by `tests.first.include?('windows')` and
68
+ `@run_data[:win_remote_module_path]` being set). The new config key
69
+ is unused — no error is raised.
70
+ - **CLI flag interacting with `-O platform.gcp.windows_bucket=foo`.**
71
+ `-O` is parsed to nested options before `--windows-bucket`. If both
72
+ are supplied, the later (in argv order) wins because `OptionParser`
73
+ processes them sequentially and both write to the same key. Not a
74
+ new pattern; matches existing flag/`-O` interactions.
75
+
76
+ ## Constraints / Invariants
77
+
78
+ - Default behavior unchanged for any existing user that does not set
79
+ `platform.gcp.windows_bucket`.
80
+ - `valid_keys` validation: `platform` is already in
81
+ `BASE_VALID_KEYS`, so nested `gcp.windows_bucket` is implicitly
82
+ accepted (validation is top-level only). No schema gate to update.
83
+ - Existing `Config::CemAcpt#defaults` already has `platform: { name: 'gcp' }`
84
+ — extend it to include `gcp: { windows_bucket: 'win_cem_acpt' }`.
85
+
86
+ ## Error Handling
87
+
88
+ - Missing/empty bucket → `RuntimeError` from `windows_bucket_uri`,
89
+ raised before any `gcloud` invocation.
90
+ - `gcloud` upload/remove failures continue to be handled exactly as
91
+ today (`Shell.run_cmd` raises on upload; `raise_on_error: false` on
92
+ cleanup).
93
+
94
+ ## Non-Goals
95
+
96
+ - Refactoring how the Windows path detects a Windows run
97
+ (`tests.first.include?('windows')`). Out of scope; covered by
98
+ separate RFCs.
99
+ - Generalizing the bucket idea to other platforms (no AWS S3, etc.).
100
+ - Validating bucket-name format (`gcloud` does that already).
101
+ - Reworking `WinNode` to take a richer config object — minimum
102
+ threading only.
103
+
104
+ ## Acceptance Criteria
105
+
106
+ From the RFC:
107
+
108
+ - [x] Config schema (`defaults`) extended with
109
+ `platform.gcp.windows_bucket = 'win_cem_acpt'`.
110
+ - [x] CLI flag `--windows-bucket BUCKET` parsed in `Cli`.
111
+ - [x] All references to the literal `'gs://win_cem_acpt'` removed
112
+ from `test_runner.rb` **and** `utils/winrm_runner.rb`.
113
+ - [x] RSpec coverage for `windows_bucket_uri` (default, override,
114
+ empty/nil) and that `upload_module_to_bucket` /
115
+ `cleanup_bucket` use the configured bucket.
116
+ - [x] `docs/ARCHITECTURE.md` §13 updated to describe the new key
117
+ and the line-220 ASCII diagram updated.
118
+ - [x] README updated under "Testing with sce_windows" to mention
119
+ the new key/flag.
120
+
121
+ ## Files Touched
122
+
123
+ - `lib/cem_acpt/config/cem_acpt.rb` — extend `platform` defaults.
124
+ - `lib/cem_acpt/cli.rb` — add `--windows-bucket` flag.
125
+ - `lib/cem_acpt/test_runner.rb` — add `windows_bucket_uri`, use it,
126
+ pass to `WinNode`.
127
+ - `lib/cem_acpt/utils/winrm_runner.rb` — accept bucket URI, drop
128
+ hardcoded literal.
129
+ - `spec/cem_acpt/test_runner_spec.rb` — coverage for the new helper
130
+ and call sites.
131
+ - `docs/ARCHITECTURE.md` — §13 prose + line-220 diagram.
132
+ - `README.md` — document new key/flag in the sce_windows section.
133
+ - `specifications/CEM-6715.md` — this file.
@@ -0,0 +1,160 @@
1
+ # CEM-6716 — Tighten `--quiet` semantics and fix logging typos
2
+
3
+ Implements RFC 0007 (`docs/rfcs/0007-logging-quiet-and-typos.md`).
4
+
5
+ ## Summary
6
+
7
+ Two related logging defects:
8
+
9
+ 1. `--quiet` is silently a no-op when used without `--log-file`. The
10
+ CI-mode override that re-adds `$stdout` is undocumented in
11
+ `--help`.
12
+ 2. `lib/cem_acpt/test_runner.rb#upload_module_to_bucket` (line 401)
13
+ references `@run_data[:module_pakage_path]` — a typo for
14
+ `module_package_path` — which interpolates to an empty string in
15
+ the log line.
16
+
17
+ Fix both, document the CI-mode override, add coverage.
18
+
19
+ ## Functional Behavior
20
+
21
+ ### `lib/cem_acpt.rb#initialize_logger!`
22
+
23
+ Rewrite the body so that:
24
+
25
+ - `--quiet` always drops `$stdout` from `logdevs`.
26
+ - `--log-file FILE` always adds the file to `logdevs`.
27
+ - If `config.ci_mode?` is true and `$stdout` was dropped, re-add it
28
+ and emit a one-time `warn`-level message that `--quiet` was
29
+ overridden so `::group::` directives can reach Actions stdout.
30
+ - If `logdevs` is still empty after the above (i.e. `--quiet` outside
31
+ CI with no `--log-file`), raise a `RuntimeError` with the message
32
+ `--quiet without --log-file would silence all output; pass --log-file or drop --quiet`.
33
+ - Verbose / level / formatter handling is unchanged.
34
+
35
+ The warn is emitted via the freshly-built logger, which is safe
36
+ because the warning destination is exactly the destination the user
37
+ will end up with (`$stdout` and/or the log file). No new "warn-once"
38
+ helper is introduced; `initialize_logger!` runs once per process, so
39
+ "once" is implicit.
40
+
41
+ ### `lib/cem_acpt/test_runner.rb#upload_module_to_bucket`
42
+
43
+ Rename `@run_data[:module_pakage_path]` → `@run_data[:module_package_path]`
44
+ in the `logger.info` call on line 401. The non-typoed key on the next
45
+ line already exists in `run_data` (set in `pre_provision_test_nodes`
46
+ at line 188).
47
+
48
+ ### CLI help (`lib/cem_acpt/cli.rb`)
49
+
50
+ Update the `-q / --quiet` description from `'Do not log to stdout'`
51
+ to mention the CI override and the requirement to pair with
52
+ `--log-file`. Target text:
53
+
54
+ ```
55
+ 'Drop stdout (requires --log-file outside CI; CI mode keeps stdout for ::group:: support)'
56
+ ```
57
+
58
+ ### Documentation (`docs/ARCHITECTURE.md` §15)
59
+
60
+ Replace the "conditional / silently a no-op" prose with the new
61
+ behavior:
62
+
63
+ - `--quiet` *with* `--log-file FILE`: stdout dropped, logs go only to
64
+ `FILE`.
65
+ - `--quiet` *without* `--log-file`: `RuntimeError` at startup with a
66
+ clear message.
67
+ - CI mode (`GITHUB_ACTIONS`, `CI`, or `-I`): `--quiet` is overridden
68
+ so `$stdout` is re-added; a single warn line is emitted at startup
69
+ noting the override.
70
+
71
+ Also remove or update the §19 item 6 ("worth a behavioral sanity
72
+ check") — it has been resolved by this fix.
73
+
74
+ ## Input/Output Contracts
75
+
76
+ ### `CemAcpt#initialize_logger!` (private, instance method on the singleton)
77
+
78
+ - **Input:** none (reads `@config`).
79
+ - **Output:** `CemAcpt::Logging::MultiLogger` (unchanged return shape).
80
+ - **Raises:**
81
+ - `RuntimeError("Config must be loaded before logger can be initialized")` — unchanged.
82
+ - `RuntimeError("--quiet without --log-file would silence all output; pass --log-file or drop --quiet")` — new.
83
+
84
+ ### `lib/cem_acpt/test_runner.rb#upload_module_to_bucket`
85
+
86
+ No signature change; only the interpolation key in the log line changes.
87
+
88
+ ## Edge Cases
89
+
90
+ | Case | `--quiet` | `--log-file` | CI mode | Result |
91
+ | --- | --- | --- | --- | --- |
92
+ | 1 | unset | unset | no | logs to `$stdout` |
93
+ | 2 | unset | set | no | logs to `$stdout` and file |
94
+ | 3 | set | unset | no | **raises** at startup |
95
+ | 4 | set | set | no | logs only to file |
96
+ | 5 | unset | unset | yes | logs to `$stdout` (CI does not add a duplicate) |
97
+ | 6 | unset | set | yes | logs to `$stdout` and file |
98
+ | 7 | set | unset | yes | logs to `$stdout` (override warn emitted) |
99
+ | 8 | set | set | yes | logs to `$stdout` and file (override warn emitted) |
100
+
101
+ CI mode is detected by `Config::Base#ci_mode?` (sees env vars
102
+ `GITHUB_ACTIONS` / `CI` or the `-I` flag); the same source of truth
103
+ as today.
104
+
105
+ ## Constraints / Invariants
106
+
107
+ - The logger is always the `MultiLogger` returned by `new_logger` —
108
+ no logger-shape changes leak to callers.
109
+ - The warn-once is emitted *after* the logger is built and *before*
110
+ `initialize_logger!` returns, so it lands in the same destinations
111
+ as the rest of the run's logs.
112
+ - Behavior outside `initialize_logger!` (level, verbose, signal
113
+ handlers, trap-context handling) is untouched.
114
+
115
+ ## Error Handling
116
+
117
+ - The new `RuntimeError` is allowed to bubble out of `run_cem_acpt` /
118
+ `run_cem_acpt_image`. `OptionParser` already surfaces option errors
119
+ by writing to stderr and exiting non-zero; we want analogous
120
+ behavior here. We do **not** catch and re-raise.
121
+
122
+ ## Non-Goals
123
+
124
+ - Promoting `--quiet` to fully silence logs in CI (would break
125
+ `::group::`).
126
+ - Adding a `--silent` / "really quiet" mode.
127
+ - Changing `--verbose`, `--debug`, or `-I` behavior.
128
+ - Refactoring `MultiLogger` or `Logger`.
129
+ - Making `logger_warn_once` a real helper — not needed because
130
+ `initialize_logger!` runs once per process.
131
+
132
+ ## Acceptance Criteria
133
+
134
+ From the RFC:
135
+
136
+ - [ ] `lib/cem_acpt.rb#initialize_logger!` rewritten; comment cites
137
+ RFC 0007.
138
+ - [ ] `:module_pakage_path` typo fixed in `test_runner.rb`.
139
+ - [ ] RSpec covers the four logging combinations listed in RFC
140
+ (`-q`, `-q --log-file`, `--log-file` alone, `-q` in CI mode)
141
+ plus the new failure case (`-q` outside CI without
142
+ `--log-file`).
143
+ - [ ] RSpec smoke test for `upload_module_to_bucket` asserts the log
144
+ line contains the actual `module_package_path`.
145
+ - [ ] `docs/ARCHITECTURE.md` §15 updated; §19 item 6 removed (or
146
+ noted as resolved).
147
+ - [ ] `--help` output for `--quiet` mentions the CI override and the
148
+ `--log-file` requirement.
149
+ - [ ] `bundle exec rake spec` and `rubocop` are clean.
150
+
151
+ ## Files Touched
152
+
153
+ - `lib/cem_acpt.rb` — rewrite `initialize_logger!`.
154
+ - `lib/cem_acpt/test_runner.rb` — fix typo in
155
+ `upload_module_to_bucket`.
156
+ - `lib/cem_acpt/cli.rb` — update `--quiet` description.
157
+ - `docs/ARCHITECTURE.md` — §15 prose, §19 item 6.
158
+ - `spec/cem_acpt_spec.rb` — `initialize_logger!` coverage.
159
+ - `spec/cem_acpt/test_runner_spec.rb` — log-line smoke test.
160
+ - `specifications/CEM-6716.md` — this file.
@@ -0,0 +1,239 @@
1
+ # CEM-6717 — Namespace dynamically-created platform classes
2
+
3
+ ## Summary
4
+
5
+ Move the dynamically-generated platform class (currently `::Gcp`) out
6
+ of the top-level constant table and into the `CemAcpt::Platform`
7
+ namespace. Also move the per-platform mixin out of top-level
8
+ (currently `::Platform`, defined in `lib/cem_acpt/platform/gcp.rb`)
9
+ into a sibling namespace `CemAcpt::Platform::Mixin::<Name>`. Use
10
+ `const_defined?(name, false)` for the cache check to avoid ancestor
11
+ lookup, and camel-case multi-word platform names correctly. Update
12
+ `docs/ARCHITECTURE.md` §6 and §19 to reflect the new behavior.
13
+
14
+ Implements RFC 0008 (`docs/rfcs/0008-namespace-platform-classes.md`),
15
+ plus the additional mixin relocation discussed during spec review.
16
+
17
+ ## Functional Behavior
18
+
19
+ ### `CemAcpt::Platform::Mixin` (new namespace)
20
+
21
+ Add a new namespace module `CemAcpt::Platform::Mixin` that holds the
22
+ per-platform mixin modules. The dynamic class will be loaded from the
23
+ sibling `CemAcpt::Platform::<Name>` constant; the mixin lives at
24
+ `CemAcpt::Platform::Mixin::<Name>`. Splitting the two avoids a
25
+ self-shadowing constant (we cannot have both
26
+ `CemAcpt::Platform::Gcp` the class and `CemAcpt::Platform::Gcp` the
27
+ mixin) and keeps both names purely descriptive.
28
+
29
+ The namespace is declared up front in `lib/cem_acpt/platform.rb`
30
+ alongside the existing `module CemAcpt::Platform`:
31
+
32
+ ```ruby
33
+ module CemAcpt
34
+ module Platform
35
+ module Mixin; end
36
+ # ... existing content
37
+ end
38
+ end
39
+ ```
40
+
41
+ ### `lib/cem_acpt/platform/gcp.rb`
42
+
43
+ Rename the module from top-level `module Platform` to
44
+ `module CemAcpt::Platform::Mixin::Gcp`. Method bodies, requires, and
45
+ the file path are unchanged.
46
+
47
+ ```ruby
48
+ # Before
49
+ module Platform
50
+ def platform_data
51
+ ...
52
+ end
53
+ ...
54
+ end
55
+
56
+ # After
57
+ module CemAcpt
58
+ module Platform
59
+ module Mixin
60
+ module Gcp
61
+ def platform_data
62
+ ...
63
+ end
64
+ ...
65
+ end
66
+ end
67
+ end
68
+ end
69
+ ```
70
+
71
+ The full body of the existing top-level `Platform` module — every
72
+ public and private method — moves verbatim into
73
+ `CemAcpt::Platform::Mixin::Gcp`.
74
+
75
+ ### `CemAcpt::Platform.platform_class(base_type, platform)`
76
+
77
+ Replace the body of `platform_class` so that:
78
+
79
+ 1. `const_name` is built from the platform name with multi-word
80
+ support: `platform.split(/[_-]/).map(&:capitalize).join`
81
+ (e.g. `gcp` → `Gcp`, `aws_govcloud` → `AwsGovcloud`,
82
+ `foo-bar` → `FooBar`).
83
+ 2. The cache check looks for the constant **directly on
84
+ `CemAcpt::Platform`**, with the second argument `false` so the
85
+ ancestor chain is not consulted:
86
+ `CemAcpt::Platform.const_defined?(const_name, false)`.
87
+ 3. On cache hit, return `CemAcpt::Platform.const_get(const_name, false)`.
88
+ 4. On miss:
89
+ a. `require_relative "platform/#{platform}"` to load the file.
90
+ b. Resolve the mixin via
91
+ `mixin = CemAcpt::Platform::Mixin.const_get(const_name, false)`.
92
+ A `NameError` here means the loaded file did not define
93
+ `CemAcpt::Platform::Mixin::<Name>` — let it propagate; the
94
+ message is already actionable.
95
+ c. Build the class with the resolved mixin:
96
+ `Class.new(baseklass) { include mixin }`.
97
+ d. Register via `CemAcpt::Platform.const_set(const_name, klass)`.
98
+ 5. No top-level `Object.const_set` call. The `::<Platform>` constant
99
+ must not be created as a side effect.
100
+
101
+ The previous `include Platform` lexical-lookup hack is removed — the
102
+ mixin is now resolved by an explicit `const_get` against the new
103
+ namespace, which is what RFC 0008 was reaching for.
104
+
105
+ ### Logging
106
+
107
+ `platform_class` already logs the cache hit / miss / final-class lines.
108
+ Keep them; they remain useful for `-d` runs. The wording can stay the
109
+ same — `class_name` becomes `const_name` in the log.
110
+
111
+ ## Input/Output Contracts
112
+
113
+ ### `CemAcpt::Platform.platform_class(base_type, platform)`
114
+ - Inputs unchanged: `base_type` is `:base` or `:test`; `platform` is a
115
+ string from the file basenames in `lib/cem_acpt/platform/`.
116
+ - Output unchanged: a `Class` object that subclasses
117
+ `CemAcpt::Platform::Base` (`:base`) or
118
+ `CemAcpt::Platform::TestBase` (`:test`).
119
+ - New invariant: the returned class is reachable via
120
+ `CemAcpt::Platform.const_get(const_name, false)` and is **not**
121
+ registered on `Object`.
122
+ - Existing error semantics preserved: invalid `base_type` raises
123
+ `CemAcpt::Platform::Error`.
124
+
125
+ ### Public API (`use`, `get`)
126
+ - No signature changes. Callers continue to receive the same class
127
+ objects. Anything that holds a reference to the returned class keeps
128
+ working; there is no top-level `::Gcp` reference to break.
129
+
130
+ ### Platform file contract (new)
131
+ - Each `lib/cem_acpt/platform/<name>.rb` MUST define
132
+ `CemAcpt::Platform::Mixin::<CamelCaseName>` and define
133
+ `#platform_data` and `#node_data` on it.
134
+ - Files are loaded by `platform_class` via `require_relative`; loading
135
+ happens at most once per platform per process.
136
+
137
+ ## Edge Cases
138
+
139
+ - **Cache hit across calls.** Memoization still works because
140
+ `CemAcpt::Platform.const_defined?(const_name, false)` returns true
141
+ on the second call. `const_get(..., false)` returns the same object.
142
+ - **Multi-word platform names.** `aws_govcloud` → `AwsGovcloud`;
143
+ `azure-arm` → `AzureArm`. Both the dynamic class
144
+ (`CemAcpt::Platform::AwsGovcloud`) and its mixin
145
+ (`CemAcpt::Platform::Mixin::AwsGovcloud`) follow the same form.
146
+ - **Same name registered elsewhere.** A future `class CemAcpt::Foo` or
147
+ top-level `class ::Gcp` will not collide because:
148
+ - We register on `CemAcpt::Platform`, not `Object`.
149
+ - We use `const_defined?(name, false)`, which ignores ancestors.
150
+ - **Mixin missing or misnamed.** If a platform file does not define
151
+ `CemAcpt::Platform::Mixin::<Name>`, `const_get` raises `NameError`.
152
+ The error is allowed to propagate — its message names the missing
153
+ constant, which is enough for a contributor to fix the file.
154
+ - **Both `:base` and `:test` requested for the same platform.** Today
155
+ the second call returns the cached class regardless of `base_type`
156
+ — the first call's `base_type` wins. This is preserved (it is
157
+ out of scope for this ticket; the RFC does not call it out).
158
+ - **Re-loading.** Tests that exercise the dynamic path multiple times
159
+ may need to clear `CemAcpt::Platform.<Name>` between examples; the
160
+ new spec covers this with a `before`/`after` that removes the
161
+ constant if defined. They should not need to clear `Object`
162
+ constants any more.
163
+
164
+ ## Constraints / Invariants
165
+
166
+ - `CemAcpt::Platform::Base` and `CemAcpt::Platform::TestBase` are
167
+ unchanged.
168
+ - `platforms` enumeration logic is unchanged.
169
+ - `const_defined?(name, false)` does not look up ancestors, so a
170
+ reopened `Object::Platform` (unlikely but possible) cannot leak in.
171
+ - The mixin contract is now explicit: each platform file defines a
172
+ module under `CemAcpt::Platform::Mixin`. There is exactly one
173
+ platform file in the repo today (`gcp.rb`), so the migration is
174
+ self-contained.
175
+
176
+ ## Error Handling
177
+
178
+ - Unknown `base_type` still raises `CemAcpt::Platform::Error` with the
179
+ existing message `"Base type #{base_type} is not supported"`.
180
+ - Unknown `platform` is still rejected by `use` / `get` before reaching
181
+ `platform_class` (existing checks).
182
+ - Missing mixin in a loaded file raises `NameError` from `const_get`.
183
+ No new error classes; the stock `NameError` message identifies the
184
+ missing constant.
185
+
186
+ ## Non-Goals
187
+
188
+ - Switching to a registration DSL (`Platform.register :gcp do ... end`).
189
+ The RFC defers this; we keep the dynamic-class approach.
190
+ - Caching `:base` and `:test` variants under separate constant names.
191
+ The cache currently keys on platform alone; preserved.
192
+ - Any change to GCP behavior. `gcp.rb`'s methods move byte-for-byte
193
+ into the new namespace.
194
+
195
+ ## Acceptance Criteria
196
+
197
+ From RFC 0008 (`docs/rfcs/0008-namespace-platform-classes.md`), plus
198
+ the mixin relocation:
199
+
200
+ - [ ] `Platform.platform_class` registers the dynamic class under
201
+ `CemAcpt::Platform`, not on `Object`.
202
+ - [ ] No top-level `::Gcp` constant is created at runtime, verified
203
+ by spec.
204
+ - [ ] No top-level `::Platform` mixin is created at runtime — the
205
+ mixin lives at `CemAcpt::Platform::Mixin::Gcp`. Verified by
206
+ spec.
207
+ - [ ] Multi-word platform names (`aws_govcloud`, `azure-arm`) produce
208
+ valid CamelCase constant names.
209
+ - [ ] `docs/ARCHITECTURE.md` §6 and §19 (item 5) updated.
210
+ - [ ] `spec/cem_acpt/platform/gcp_spec.rb` updated to reference the
211
+ new mixin name (`CemAcpt::Platform::Mixin::Gcp`) and continues
212
+ to pass.
213
+
214
+ ## Files Touched
215
+
216
+ - `lib/cem_acpt/platform.rb` — declare `module Mixin` namespace;
217
+ rewrite `platform_class` per the proposal.
218
+ - `lib/cem_acpt/platform/gcp.rb` — relocate the mixin from top-level
219
+ `module Platform` to `CemAcpt::Platform::Mixin::Gcp`.
220
+ - `spec/cem_acpt/platform_spec.rb` — **new** spec covering the
221
+ namespace, const-defined behavior, cache, multi-word naming, and
222
+ the absence of top-level `::Gcp` / `::Platform` constants.
223
+ - `spec/cem_acpt/platform/gcp_spec.rb` — update `RSpec.describe`,
224
+ `host.extend(...)`, and any other references from `Platform` to
225
+ `CemAcpt::Platform::Mixin::Gcp`.
226
+ - `docs/ARCHITECTURE.md` — §6 prose update (drop the "top-level
227
+ constant `Gcp`" note, describe the new
228
+ `CemAcpt::Platform::Gcp` / `CemAcpt::Platform::Mixin::Gcp` split)
229
+ and §19 item 5 update / removal.
230
+ - `specifications/CEM-6717.md` — this file.
231
+
232
+ ## Test Plan
233
+
234
+ 1. `bundle exec rake spec SPEC=spec/cem_acpt/platform_spec.rb` for the
235
+ new namespacing tests.
236
+ 2. `bundle exec rake spec SPEC=spec/cem_acpt/platform/gcp_spec.rb` to
237
+ confirm the GCP mixin spec passes after the rename.
238
+ 3. `bundle exec rake spec` for the full suite.
239
+ 4. `rubocop lib/cem_acpt/platform.rb lib/cem_acpt/platform/gcp.rb spec/cem_acpt/platform_spec.rb spec/cem_acpt/platform/gcp_spec.rb`.
@@ -0,0 +1,120 @@
1
+ # CEM-6718 — Wire up or remove orphaned Bolt log formatters
2
+
3
+ Implements RFC 0009 (`docs/rfcs/0009-bolt-log-formatter-cleanup.md`),
4
+ **Option B** (delete the orphaned formatters).
5
+
6
+ ## Summary
7
+
8
+ Two formatter classes — `BoltOutputFormatter` and `BoltErrorFormatter`
9
+ — exist under `lib/cem_acpt/test_runner/log_formatter/` but are never
10
+ matched by `LogFormatter.new_formatter`'s `case`. `BoltOutputFormatter`
11
+ is `require_relative`'d in `log_formatter.rb` but unused;
12
+ `BoltErrorFormatter` is neither required nor referenced. Both have
13
+ shipped in this dead state for several releases.
14
+
15
+ Per the RFC's recommendation, delete both files and the dangling
16
+ `require_relative`. Add a one-line comment on
17
+ `BoltSummaryResultsFormatter` documenting that it is the canonical
18
+ Bolt formatter. Update `docs/ARCHITECTURE.md` §11 and §19 to reflect
19
+ the cleanup.
20
+
21
+ ## Functional Behavior
22
+
23
+ ### File deletions
24
+
25
+ - Delete `lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb`.
26
+ - Delete `lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb`.
27
+
28
+ ### `lib/cem_acpt/test_runner/log_formatter.rb`
29
+
30
+ Remove the dangling `require_relative`:
31
+
32
+ ```ruby
33
+ require_relative 'log_formatter/bolt_output_formatter' # remove
34
+ ```
35
+
36
+ The `case` dispatch is unchanged — it already routes
37
+ `CemAcpt::Bolt::SummaryResults` to `BoltSummaryResultsFormatter` and
38
+ falls through to `StandardErrorFormatter` for `StandardError`.
39
+
40
+ ### `lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb`
41
+
42
+ Add a one-line YARD comment above the class declaration noting that
43
+ it is the canonical Bolt formatter. Replace the existing one-line
44
+ comment so the file does not gain extra noise. Target text:
45
+
46
+ ```ruby
47
+ # Canonical formatter for Bolt subsystem results. Wired into
48
+ # LogFormatter.new_formatter for CemAcpt::Bolt::SummaryResults.
49
+ class BoltSummaryResultsFormatter < Base
50
+ ```
51
+
52
+ ### `docs/ARCHITECTURE.md`
53
+
54
+ **§11 (Result aggregation & log formatting):** drop the paragraph at
55
+ lines 691–694 that calls out `BoltOutputFormatter` /
56
+ `BoltErrorFormatter` as orphaned files. The formatter table above it
57
+ is unchanged.
58
+
59
+ **§19 (item 6):** remove item 6 entirely (the "BoltOutputFormatter
60
+ and BoltErrorFormatter are dead" entry). Renumber the subsequent item
61
+ (currently item 7, "`lib/terraform/image/gcp/windows/` is an empty
62
+ directory") to item 6.
63
+
64
+ ## Input/Output Contracts
65
+
66
+ No public-API changes. The dispatcher signature
67
+ `LogFormatter.new_formatter(result, *args, **_kwargs)` and its return
68
+ contract are unchanged. The deleted classes were never wired in; no
69
+ caller constructs them.
70
+
71
+ ## Edge Cases
72
+
73
+ - A caller that pushes a hypothetical `Bolt::Cmd::Output` or
74
+ `Bolt::Cmd::OutputError` onto the results queue would have hit the
75
+ `StandardError` branch (or the `else` raise) before this change and
76
+ continues to do so after. Behavior is unchanged.
77
+ - No spec file references either deleted class (verified by grep);
78
+ removing them does not break the test suite.
79
+
80
+ ## Constraints
81
+
82
+ - RuboCop must remain clean.
83
+ - `bundle exec rake spec` must pass with no new failures.
84
+
85
+ ## Invariants
86
+
87
+ - `LogFormatter.new_formatter` continues to dispatch the same four
88
+ result types it dispatches today (`Goss::Api::ActionResponse`,
89
+ `Bolt::SummaryResults`, `StandardError`, else-raise).
90
+ - The remaining formatter files under
91
+ `lib/cem_acpt/test_runner/log_formatter/` (`base.rb`,
92
+ `bolt_summary_results_formatter.rb`, `goss_action_response.rb`,
93
+ `goss_error_formatter.rb`, `standard_error_formatter.rb`) are
94
+ unchanged in behavior.
95
+
96
+ ## Error Handling
97
+
98
+ No new error paths. The pre-existing `else raise ArgumentError`
99
+ branch in `new_formatter` is unchanged.
100
+
101
+ ## Non-Goals
102
+
103
+ - Re-implementing `BoltOutputFormatter` / `BoltErrorFormatter`. If the
104
+ need re-emerges, RFC 0009 documents Option A as the alternative
105
+ path; reintroducing them would be a small, separate patch.
106
+ - Refactoring or restructuring `BoltSummaryResultsFormatter` itself.
107
+ - Touching the Goss formatters.
108
+ - Updating the RFC's status (the RFC remains the historical record of
109
+ the decision; the implementation closes the ticket).
110
+
111
+ ## Acceptance Criteria
112
+
113
+ - [ ] `lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb` deleted.
114
+ - [ ] `lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb` deleted.
115
+ - [ ] `require_relative 'log_formatter/bolt_output_formatter'` removed from `lib/cem_acpt/test_runner/log_formatter.rb`.
116
+ - [ ] One-line comment added to `BoltSummaryResultsFormatter`.
117
+ - [ ] `docs/ARCHITECTURE.md` §11 paragraph about orphaned formatters removed.
118
+ - [ ] `docs/ARCHITECTURE.md` §19 item 6 removed; subsequent item renumbered.
119
+ - [ ] `bundle exec rake spec` passes.
120
+ - [ ] `rubocop` is clean.