opensearch-sugar 1.0.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.agents/skills/diataxis/SKILL.md +142 -0
  3. data/.agents/skills/diataxis/references/examples.md +420 -0
  4. data/.agents/skills/diataxis/references/explanation-template.md +96 -0
  5. data/.agents/skills/diataxis/references/framework.md +400 -0
  6. data/.agents/skills/diataxis/references/how-to-guide-template.md +105 -0
  7. data/.agents/skills/diataxis/references/reference-template.md +110 -0
  8. data/.agents/skills/diataxis/references/tutorial-template.md +101 -0
  9. data/.agents/skills/diataxis/scripts/generate_index.py +139 -0
  10. data/.rspec +3 -0
  11. data/.standard.yml +3 -0
  12. data/AGENTS.md +120 -0
  13. data/CHANGELOG.md +5 -0
  14. data/Dockerfile.opensearch +4 -0
  15. data/Increase_Coverage.md +311 -0
  16. data/README.md +143 -0
  17. data/Rakefile +27 -0
  18. data/Steepfile +23 -0
  19. data/adrs/ADR-000-template.md +87 -0
  20. data/adrs/ADR-001-simpledelegator-for-client.md +138 -0
  21. data/adrs/ADR-002-facade-pattern-for-index.md +126 -0
  22. data/adrs/ADR-003-repository-pattern-for-models.md +148 -0
  23. data/adrs/ADR-004-integration-tests-no-mocking.md +91 -0
  24. data/adrs/ADR-005-exceptions-over-result-objects.md +107 -0
  25. data/adrs/ADR-006-ssl-on-by-default.md +95 -0
  26. data/adrs/ADR-007-selective-sugar-surface.md +118 -0
  27. data/adrs/ADR-008-integration-test-design.md +178 -0
  28. data/compose.yml +2 -0
  29. data/compose_opensearch.yml +31 -0
  30. data/docs/HOWTO.md +844 -0
  31. data/docs/REFERENCE.md +725 -0
  32. data/docs/TUTORIAL.md +327 -0
  33. data/docs/alias-api-design-notes.md +119 -0
  34. data/lib/opensearch/sugar/client.rb +300 -0
  35. data/lib/opensearch/sugar/index/include/utilities.rb +6 -0
  36. data/lib/opensearch/sugar/index.rb +339 -0
  37. data/lib/opensearch/sugar/models.rb +209 -0
  38. data/lib/opensearch/sugar/version.rb +8 -0
  39. data/lib/opensearch/sugar.rb +61 -0
  40. data/old_docs/DELEGATED_METHODS_ANALYSIS.md +361 -0
  41. data/old_docs/EXPLANATION.md +685 -0
  42. data/old_docs/README.md +155 -0
  43. data/old_docs/docs/CLI-PROPOSAL.md +257 -0
  44. data/old_docs/docs/HOWTO.md +798 -0
  45. data/old_docs/docs/REFERENCE.md +901 -0
  46. data/old_docs/docs/TUTORIAL.md +493 -0
  47. data/sig/opensearch/sugar.rbs +162 -0
  48. metadata +240 -0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to generate index.md for Diátaxis-organized documentation.
4
+
5
+ Scans the specified docs root (default .docs) for subfolders: tutorials, how-to, reference, explanation.
6
+ Recursively renders nested sub-category folders (e.g., reference/ralph/, how-to/copilot/cli/).
7
+ Extracts titles from .md files and generates an index.md file with links organized by category
8
+ and sub-category.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ def get_title(filepath):
17
+ """Extract the title from the first line of a markdown file."""
18
+ try:
19
+ with open(filepath, 'r', encoding='utf-8') as f:
20
+ first_line = f.readline().strip()
21
+ if first_line.startswith('# '):
22
+ return first_line[2:].strip()
23
+ else:
24
+ return os.path.basename(filepath).replace('.md', '').replace('-', ' ').title()
25
+ except Exception as e:
26
+ print(f"Error reading {filepath}: {e}")
27
+ return os.path.basename(filepath).replace('.md', '').replace('-', ' ').title()
28
+
29
+
30
+ def has_visible_content(directory):
31
+ """Return True when the directory contains visible files or subdirectories."""
32
+ for entry in directory.iterdir():
33
+ if entry.name == '.gitkeep':
34
+ return True
35
+ if not entry.name.startswith('.'):
36
+ return True
37
+ return False
38
+
39
+
40
+ def render_directory(directory, docs_root, heading_level):
41
+ """Render a directory and any nested subdirectories recursively."""
42
+ direct_files = sorted(
43
+ f for f in directory.iterdir()
44
+ if f.is_file() and f.suffix == '.md'
45
+ )
46
+ child_dirs = sorted(
47
+ d for d in directory.iterdir()
48
+ if d.is_dir() and not d.name.startswith('.')
49
+ )
50
+ visible_children = [d for d in child_dirs if has_visible_content(d)]
51
+ has_keep_marker = any(entry.name == '.gitkeep' for entry in directory.iterdir())
52
+
53
+ if not direct_files and not visible_children and not has_keep_marker:
54
+ return ""
55
+
56
+ heading = directory.name.replace('-', ' ').title()
57
+ content = f"{'#' * heading_level} {heading}\n\n"
58
+
59
+ for file in direct_files:
60
+ title = get_title(str(file))
61
+ link = f"{directory.relative_to(docs_root).as_posix()}/{file.name}"
62
+ content += f"- [{title}]({link})\n"
63
+
64
+ if direct_files and visible_children:
65
+ content += "\n"
66
+
67
+ if not direct_files and not visible_children:
68
+ content += "- _No documents yet._\n\n"
69
+
70
+ for child_dir in visible_children:
71
+ child_content = render_directory(child_dir, docs_root, heading_level + 1)
72
+ if child_content:
73
+ content += child_content
74
+
75
+ return content
76
+
77
+
78
+ def main(docs_root='.docs'):
79
+ """Generate the index.md file."""
80
+ categories = {
81
+ 'tutorials': 'Tutorials',
82
+ 'how-to': 'How-to Guides',
83
+ 'reference': 'Reference',
84
+ 'explanation': 'Explanation'
85
+ }
86
+
87
+ index_content = """# Copilot Workspace Documentation Index
88
+
89
+ This index links Diátaxis-organized documentation for the workspace.
90
+
91
+ """
92
+
93
+ docs_root_path = Path(docs_root)
94
+
95
+ for folder, section in categories.items():
96
+ cat_path = docs_root_path / folder
97
+ if not cat_path.is_dir():
98
+ continue
99
+
100
+ root_files = sorted(
101
+ f for f in cat_path.iterdir()
102
+ if f.is_file() and f.suffix == '.md'
103
+ )
104
+ subdirs = sorted(
105
+ d for d in cat_path.iterdir()
106
+ if d.is_dir() and not d.name.startswith('.')
107
+ )
108
+ visible_subdirs = [d for d in subdirs if has_visible_content(d)]
109
+
110
+ if not root_files and not visible_subdirs:
111
+ continue
112
+
113
+ index_content += f"## {section}\n\n"
114
+
115
+ for file in root_files:
116
+ title = get_title(str(file))
117
+ link = f"{folder}/{file.name}"
118
+ index_content += f"- [{title}]({link})\n"
119
+
120
+ if root_files and visible_subdirs:
121
+ index_content += "\n"
122
+
123
+ for subdir in visible_subdirs:
124
+ index_content += render_directory(subdir, docs_root_path, 3)
125
+ if not index_content.endswith("\n\n"):
126
+ index_content += "\n"
127
+
128
+ if not root_files:
129
+ index_content += "\n"
130
+
131
+ index_path = os.path.join(docs_root, 'index.md')
132
+ index_content = index_content.rstrip() + "\n"
133
+ with open(index_path, 'w', encoding='utf-8') as f:
134
+ f.write(index_content)
135
+ print(f"Generated {index_path}")
136
+
137
+ if __name__ == '__main__':
138
+ docs_root = sys.argv[1] if len(sys.argv) > 1 else '.docs'
139
+ main(docs_root)
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/AGENTS.md ADDED
@@ -0,0 +1,120 @@
1
+ # opensearch-sugar — Agent Instructions
2
+
3
+ ## What this project is
4
+
5
+ `opensearch-sugar` is a Ruby gem that wraps the [`opensearch-ruby`](https://github.com/opensearch-project/opensearch-ruby)
6
+ client with a more convenient, object-oriented API. It provides three main classes:
7
+
8
+ - **`OpenSearch::Sugar::Client`** — connects to an OpenSearch cluster; adds index management,
9
+ settings/mappings helpers, and delegated access to all underlying `OpenSearch::Client` methods
10
+ - **`OpenSearch::Sugar::Index`** — represents a single index with methods for CRUD on documents,
11
+ settings, mappings, aliases, and text analysis
12
+ - **`OpenSearch::Sugar::Models`** — manages ML models via the OpenSearch ML Commons plugin
13
+ (register, deploy, list, delete, and build embedding pipelines)
14
+
15
+ This is a plain Ruby gem. It has no Rails dependency and must not use Rails-specific gems or patterns.
16
+
17
+ ## File structure
18
+
19
+ ```
20
+ lib/
21
+ opensearch/
22
+ sugar.rb # Top-level require; defines OpenSearch::Sugar module
23
+ sugar/
24
+ client.rb # OpenSearch::Sugar::Client (SimpleDelegator wrapper)
25
+ index.rb # OpenSearch::Sugar::Index
26
+ index/ # Index sub-components
27
+ models.rb # OpenSearch::Sugar::Models
28
+ version.rb # OpenSearch::Sugar::VERSION
29
+
30
+ sig/
31
+ opensearch/
32
+ sugar.rbs # RBS type signatures for the public API
33
+
34
+ spec/
35
+ spec_helper.rb
36
+ opensearch_integration_spec.rb
37
+ env.testing # dotenv file for the test cluster
38
+
39
+ docs/
40
+ REFERENCE.md # Complete API reference
41
+ HOWTO.md # Problem-solving recipes
42
+ TUTORIAL.md # Step-by-step learning guide
43
+ EXPLANATION.md # Conceptual background
44
+ DELEGATED_METHODS_ANALYSIS.md
45
+
46
+ adrs/ # Architecture Decision Records
47
+ ```
48
+
49
+ ## Code style
50
+
51
+ - **Formatter**: Ruby `standard` gem with default settings. Config at `.standard.yml`. Do not suppress rules without a comment explaining why.
52
+ - **Line length**: 100 characters (`.editorconfig`).
53
+
54
+ ## Test stack
55
+
56
+ Tests are written with **RSpec** and live in `spec/`. They are **integration tests** that
57
+ require a live OpenSearch node; there is no mocking layer. Spin up a local cluster first:
58
+
59
+ ```bash
60
+ docker compose up -d
61
+ bundle exec rspec
62
+ ```
63
+
64
+ Environment variables for the test cluster are loaded from `spec/env.testing` via `dotenv`.
65
+ There is currently no unit-test suite; if you add pure-Ruby logic to `lib/` that can be
66
+ tested in isolation, add unit specs alongside it.
67
+
68
+ ## Naming conventions
69
+
70
+ - **Top-level namespace**: `OpenSearch::Sugar`
71
+ - **Classes**: `OpenSearch::Sugar::Client`, `OpenSearch::Sugar::Index`, `OpenSearch::Sugar::Models`
72
+ - **Error base class**: `OpenSearch::Sugar::Error < StandardError`
73
+ - **Source files**: `snake_case` under `lib/opensearch/sugar/` (e.g. `client.rb`, `models.rb`)
74
+ - **Spec files**: mirror source path with `_spec.rb` suffix (e.g. `spec/opensearch/sugar/client_spec.rb`)
75
+ - **RBS files**: `snake_case` under `sig/opensearch/sugar/` with `.rbs` extension
76
+ - **Script/rake filenames**: `snake_case` (Ruby: `.rb`, bash: `.sh`)
77
+
78
+ ## Type signatures
79
+
80
+ RBS type signatures for the public API live in `sig/`. When adding or changing public methods,
81
+ update the corresponding `.rbs` file to keep signatures in sync. Type-check with `steep check`
82
+ if Steep is configured, or validate signatures with `rbs validate`.
83
+
84
+ ## Documentation
85
+
86
+ - General documentation conventions follow `/Users/dueberb/devel/ai/agent_resources/external_documentation.md`.
87
+ - Project metadata conventions (versioning, changelog, release process, CONTRIBUTING, SECURITY) follow
88
+ `/Users/dueberb/devel/ai/agent_resources/project_metadata.md`.
89
+ - **Diagrams**: use Mermaid in both ADRs and `docs/` files.
90
+
91
+ ## Ruby idioms and best practices
92
+
93
+ - Use modern Ruby syntax and idioms (post-3.4) unless there is a compelling reason not to.
94
+ - Avoid `_1`-style block variables for readability.
95
+ - Use exceptions for error handling rather than Result objects or similar patterns, unless there is a compelling reason to do otherwise.
96
+ - Don't use Rails-specific gems or patterns, as this is a general-purpose Ruby gem that does not assume a Rails environment.
97
+ - Use YARD for API documentation, with clear comments on all public methods and classes. Include links to relevant OpenSearch documentation where applicable.
98
+
99
+ ## ADRs
100
+
101
+ All design decisions are recorded in `adrs/`. Use the template in `adrs/ADR-000-template.md` for consistency.
102
+ When making a non-trivial architectural
103
+ choice, check the ADRs first. If a decision conflicts with an ADR, raise it explicitly
104
+ rather than quietly working around it.
105
+
106
+ ## Where things go
107
+
108
+ | What | Where | Notes |
109
+ |---|---|---|
110
+ | Public API documentation | `docs/REFERENCE.md` | Method signatures, parameters, return values |
111
+ | How-to recipes | `docs/HOWTO.md` | Task-oriented guides for common problems |
112
+ | Tutorial content | `docs/TUTORIAL.md` | Step-by-step learning walkthroughs |
113
+ | Conceptual background | `docs/EXPLANATION.md` | Why things work the way they do |
114
+ | Architecture decisions | `adrs/` | Use `ADR-NNN-title.md` format; scaffold with the `adr-create` skill |
115
+ | Agent/AI working plans | project root | Uppercase name, e.g. `Increase_Coverage.md`; these are working documents, not user-facing docs |
116
+ | Gem source code | `lib/opensearch/sugar/` | `snake_case.rb`; mirror namespace in directory structure |
117
+ | Spec files | `spec/opensearch/sugar/` | Mirror source path with `_spec.rb` suffix |
118
+ | RBS type signatures | `sig/opensearch/sugar/` | Mirror source path with `.rbs` suffix |
119
+ | Rake tasks | `Rakefile` | Keep tasks focused; add a `desc` to every task |
120
+ | Gem dependencies | `opensearch-sugar.gemspec` | Runtime deps via `add_dependency`; dev deps via `add_development_dependency` |
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-06-19
4
+
5
+ - Initial release
@@ -0,0 +1,4 @@
1
+ FROM opensearchproject/opensearch:latest AS opensearch
2
+
3
+ RUN bin/opensearch-plugin install analysis-icu
4
+
@@ -0,0 +1,311 @@
1
+ # Plan: Increase Test Coverage - Phase 1 Quick Wins
2
+
3
+ ## Current Status
4
+
5
+ - **Overall Coverage:** 78.2% (165/211 lines)
6
+ - **By File:**
7
+ - `opensearch/sugar/models.rb`: 25.9% (14/54 lines) - Intentionally excluded (`:models` tag)
8
+ - `opensearch/sugar/client.rb`: 93.8% (60/64 lines) - 4 uncovered lines
9
+ - `opensearch/sugar/index.rb`: 97.5% (78/80 lines) - 2 uncovered lines
10
+
11
+ ## Objective
12
+
13
+ Increase test coverage from **78.2%** to **~82%** by adding tests for uncovered code paths in the Client and Index classes. This focuses on achievable, high-value improvements without requiring ML plugin setup or complex edge case testing.
14
+
15
+ ---
16
+
17
+ ## Phase 1: Quick Wins (Target: +3-4% coverage)
18
+
19
+ ### 1. Add Unwrapped Settings Format Test
20
+
21
+ **File:** `spec/opensearch/sugar/client/settings_spec.rb`
22
+ **Coverage target:** `client.rb:181`
23
+ **Issue:** Tests only use wrapped format `{ settings: { ... } }`. Need to test unwrapped format `{ analysis: { ... } }`.
24
+
25
+ **Implementation:**
26
+
27
+ Add a new describe block to test unwrapped settings format:
28
+
29
+ ```ruby
30
+ describe "#update_settings with unwrapped format" do
31
+ let(:index_name) { "sugar_test_#{SecureRandom.hex(6)}" }
32
+ let(:analyzer_settings_unwrapped) do
33
+ {
34
+ analysis: {
35
+ analyzer: {
36
+ test_unwrapped: {
37
+ type: "custom",
38
+ tokenizer: "standard",
39
+ filter: ["lowercase"]
40
+ }
41
+ }
42
+ }
43
+ }
44
+ end
45
+
46
+ before { client.create_index!(index_name) }
47
+ after { client.delete_index!(index_name) rescue nil }
48
+
49
+ it "accepts settings without wrapping in 'settings' key" do
50
+ expect { client.update_settings(analyzer_settings_unwrapped, index_name) }.not_to raise_error
51
+ end
52
+
53
+ it "applies the unwrapped settings correctly" do
54
+ client.update_settings(analyzer_settings_unwrapped, index_name)
55
+ index = client.open_index(index_name)
56
+ expect(index.all_available_analyzers).to include("test_unwrapped")
57
+ end
58
+ end
59
+ ```
60
+
61
+ ---
62
+
63
+ ### 2. Add Unwrapped Mappings Format Test
64
+
65
+ **File:** `spec/opensearch/sugar/index/mappings_spec.rb`
66
+ **Coverage target:** `client.rb:218`
67
+ **Issue:** Tests only use wrapped format `{ mappings: { ... } }`. Need to test unwrapped format `{ properties: { ... } }`.
68
+
69
+ **Implementation:**
70
+
71
+ Add a new describe block to test unwrapped mappings format:
72
+
73
+ ```ruby
74
+ describe "#update_mappings with unwrapped format" do
75
+ let(:new_mappings_unwrapped) do
76
+ {
77
+ properties: {
78
+ unwrapped_title: { type: "text" },
79
+ unwrapped_count: { type: "integer" }
80
+ }
81
+ }
82
+ end
83
+
84
+ it "accepts mappings without wrapping in 'mappings' key" do
85
+ expect { index.update_mappings(new_mappings_unwrapped) }.not_to raise_error
86
+ end
87
+
88
+ it "makes the new fields visible in mappings after the update" do
89
+ index.update_mappings(new_mappings_unwrapped)
90
+ props = index.mappings.dig(index_name, "mappings", "properties")
91
+ expect(props).to include("unwrapped_title", "unwrapped_count")
92
+ end
93
+ end
94
+ ```
95
+
96
+ ---
97
+
98
+ ### 3. Add Synonym Analyzer Test (Client)
99
+
100
+ **File:** `spec/opensearch/sugar/client/analyzer_spec.rb`
101
+ **Coverage target:** `client.rb:280`
102
+ **Issue:** Missing test for tokens at the same position (synonyms, word decomposition).
103
+
104
+ **Implementation:**
105
+
106
+ Add a test case that uses a synonym filter to generate same-position tokens:
107
+
108
+ ```ruby
109
+ describe "#test_analyzer_by_definition with synonym filter" do
110
+ it "returns tokens at the same position as arrays when synonyms are used" do
111
+ tokens = client.test_analyzer_by_definition(
112
+ text: "quick brown fox",
113
+ tokenizer: "standard",
114
+ filter: [
115
+ {
116
+ type: "synonym",
117
+ synonyms: ["quick, fast"]
118
+ }
119
+ ]
120
+ )
121
+
122
+ # Should return: [["quick", "fast"], "brown", "fox"] or similar
123
+ # The exact format depends on synonym expansion behavior
124
+ expect(tokens).to be_an(Array)
125
+ expect(tokens.first).to be_an(Array).or be_a(String)
126
+ end
127
+
128
+ it "handles multi-word synonyms that produce same-position tokens" do
129
+ tokens = client.test_analyzer_by_definition(
130
+ text: "wi-fi",
131
+ tokenizer: "standard",
132
+ filter: [
133
+ {
134
+ type: "word_delimiter",
135
+ split_on_case_change: false,
136
+ split_on_numerics: false
137
+ }
138
+ ]
139
+ )
140
+
141
+ expect(tokens).to be_an(Array)
142
+ end
143
+ end
144
+ ```
145
+
146
+ ---
147
+
148
+ ### 4. Add Synonym Analyzer Test (Index)
149
+
150
+ **File:** `spec/opensearch/sugar/index/analyzer_spec.rb`
151
+ **Coverage target:** `index.rb:210`
152
+ **Issue:** Missing test for tokens at the same position (synonyms, word decomposition).
153
+
154
+ **Implementation:**
155
+
156
+ Add a test case that creates an analyzer with synonyms:
157
+
158
+ ```ruby
159
+ describe "#test_analyzer_by_name with synonym filter" do
160
+ let(:synonym_analyzer_settings) do
161
+ {
162
+ settings: {
163
+ analysis: {
164
+ filter: {
165
+ synonym_filter: {
166
+ type: "synonym",
167
+ synonyms: ["quick, fast", "big, large"]
168
+ }
169
+ },
170
+ analyzer: {
171
+ synonym_analyzer: {
172
+ type: "custom",
173
+ tokenizer: "standard",
174
+ filter: ["lowercase", "synonym_filter"]
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ end
181
+
182
+ let(:index_with_synonyms) do
183
+ idx = OpenSearch::Sugar::Index.create(client: client, name: index_name)
184
+ idx.update_settings(synonym_analyzer_settings)
185
+ idx
186
+ end
187
+
188
+ before { index_with_synonyms }
189
+
190
+ it "returns tokens at the same position as arrays when synonyms expand" do
191
+ tokens = index_with_synonyms.test_analyzer_by_name(
192
+ analyzer: "synonym_analyzer",
193
+ text: "quick brown fox"
194
+ )
195
+
196
+ # Should include both "quick" and "fast" at position 0
197
+ expect(tokens).to be_an(Array)
198
+ flattened = tokens.flatten
199
+ expect(flattened).to include("quick").or include("fast")
200
+ end
201
+ end
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Expected Outcome
207
+
208
+ After implementing Phase 1:
209
+ - **Estimated coverage:** ~82% (up from 78.2%)
210
+ - **Lines covered:** +8-10 lines across Client and Index classes
211
+ - **Test execution time:** Minimal increase (~2-3 seconds)
212
+ - **Files modified:** 3 spec files
213
+
214
+ ---
215
+
216
+ ## What We're NOT Doing (And Why)
217
+
218
+ ### Phase 2: Model Tests - Skipped for Now
219
+
220
+ **Why we're skipping:**
221
+ - Model tests exist but are excluded by default (`:models` tag in `spec_helper.rb:34`)
222
+ - Requires ML Commons plugin setup in the test cluster
223
+ - Tests are marked `:slow` and take significantly longer to run
224
+ - The 40 uncovered lines in `models.rb` represent 25.9% coverage, but this is a separate testing concern
225
+ - Model functionality may require a different testing strategy (mocking, VCR, or dedicated ML test environment)
226
+
227
+ **Current model test files (not running):**
228
+ - `spec/opensearch/sugar/models/registration_spec.rb` (38 lines)
229
+ - `spec/opensearch/sugar/models/lookup_spec.rb` (57 lines)
230
+ - `spec/opensearch/sugar/models/lifecycle_spec.rb` (40 lines)
231
+ - `spec/opensearch/sugar/models/pipeline_spec.rb` (50 lines)
232
+
233
+ **When to revisit:**
234
+ - When ML Commons plugin is consistently available in CI
235
+ - When we decide on a testing strategy for slow/external-dependency tests
236
+ - When we want to push coverage above 85%
237
+
238
+ ---
239
+
240
+ ### Phase 3: Edge Cases - Skipped for Now
241
+
242
+ **Why we're skipping:**
243
+
244
+ 1. **Double-failure recovery (`client.rb:297`)**
245
+ - Requires forcing two consecutive failures (update fails AND reopen fails)
246
+ - Complex to test reliably in integration tests
247
+ - Defensive code with low user impact
248
+ - ROI: Low - edge case that's rarely hit in practice
249
+
250
+ 2. **Default index body (`index.rb:331`)**
251
+ - Private method, already indirectly tested
252
+ - All current tests explicitly provide settings
253
+ - SimpleCov may not track private method calls correctly
254
+ - ROI: Very low - method IS executed, just not explicitly tested
255
+
256
+ **When to revisit:**
257
+ - When aiming for 90%+ coverage
258
+ - When edge case bugs are discovered
259
+ - When refactoring makes these paths more testable
260
+
261
+ ---
262
+
263
+ ## Implementation Checklist
264
+
265
+ - [x] Add unwrapped settings format test to `spec/opensearch/sugar/client/settings_spec.rb`
266
+ - [x] Add unwrapped mappings format test to `spec/opensearch/sugar/index/mappings_spec.rb`
267
+ - [x] Add synonym analyzer test to `spec/opensearch/sugar/client/analyzer_spec.rb`
268
+ - [x] Add synonym analyzer test to `spec/opensearch/sugar/index/analyzer_spec.rb`
269
+ - [x] Run coverage: `bundle exec rake coverage`
270
+ - [x] Verify new coverage percentage — **actual result: 80.1%** (169/211 lines, up from 78.2%)
271
+ - [x] Commit changes
272
+ - [x] Update this document with actual results
273
+
274
+ **Note:** Coverage reached 80.1% rather than the estimated 82%. The synonym tests exercised the
275
+ same-position token branch but the remaining 2 uncovered lines (client.rb:297 double-failure rescue,
276
+ index.rb:331 private default body) are in Phase 3 (skipped).
277
+
278
+ ---
279
+
280
+ ## Running Coverage
281
+
282
+ To generate a coverage report after implementation:
283
+
284
+ ```bash
285
+ # Run specs with coverage
286
+ bundle exec rake coverage
287
+
288
+ # Or with environment variable
289
+ COVERAGE=true bundle exec rspec
290
+
291
+ # View report
292
+ open coverage/index.html
293
+ ```
294
+
295
+ To run only the new tests:
296
+
297
+ ```bash
298
+ bundle exec rspec spec/opensearch/sugar/client/settings_spec.rb
299
+ bundle exec rspec spec/opensearch/sugar/index/mappings_spec.rb
300
+ bundle exec rspec spec/opensearch/sugar/client/analyzer_spec.rb
301
+ bundle exec rspec spec/opensearch/sugar/index/analyzer_spec.rb
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Notes
307
+
308
+ - All Phase 1 tests are integration tests requiring a running OpenSearch cluster
309
+ - Use `docker compose up -d` to start the test cluster before running specs
310
+ - The synonym filter tests may need adjustment based on actual OpenSearch synonym behavior
311
+ - Coverage percentages are approximate and may vary slightly based on SimpleCov's line tracking