Skip to content

Commit 0cafcd4

Browse files
yaturnerclaude
andcommitted
ADFA-4484 Add TEST_SUITE.md documenting all written unit tests
Describes each test file, the class under test, and what every individual test case verifies — 93 tests across git-core, agent, common, app, and lsp:indexing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d8281a1 commit 0cafcd4

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

‎TEST_SUITE.md‎

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# Test Suite — CodeOnTheGo
2+
3+
Unit tests written for the ADFA-4484 branch. All run with:
4+
5+
```bash
6+
JAVA_HOME=/home/yaturner/.jdk17 PATH=/home/yaturner/.jdk17/bin:$PATH ./gradlew \
7+
:git-core:testV8DebugUnitTest \
8+
:common:testV8DebugUnitTest \
9+
:app:testV8DebugUnitTest \
10+
:lsp:indexing:testV8DebugUnitTest
11+
```
12+
13+
---
14+
15+
## `git-core` — Git Repository URL Parsing
16+
17+
**File:** `git-core/src/test/java/com/itsaky/androidide/git/core/GitRepositoryUrlsTest.kt`
18+
19+
Tests `parseGitRepositoryUrl()`, which validates and normalises a string into a Git remote URL or returns `null` if the input is not a recognisable repository address.
20+
21+
| Test | What it checks |
22+
|------|----------------|
23+
| blank string returns null | Empty string → `null` |
24+
| whitespace-only string returns null | String of spaces → `null` |
25+
| newline-only string returns null | `\n\t` → `null` |
26+
| valid HTTPS GitHub URL is accepted | Standard `https://github.com/…` URL is parsed and contains the host |
27+
| valid HTTPS URL with trailing whitespace is trimmed and accepted | Leading/trailing spaces are stripped before parsing |
28+
| HTTPS URL without git suffix is accepted | URL without `.git` suffix is still valid |
29+
| HTTPS URL with port is accepted | Non-standard port (`:8443`) is accepted |
30+
| HTTP URL is accepted | Plain `http://` scheme works |
31+
| SSH URL with scheme is accepted | `ssh://git@github.com/…` format works |
32+
| SCP-style SSH URL is accepted | `git@github.com:user/repo.git` format works |
33+
| SCP-style SSH URL for GitLab is accepted | Same format works for GitLab host |
34+
| SCP-style SSH URL with subdomain is accepted | Bitbucket host with subdomain works |
35+
| git protocol URL is accepted | `git://` scheme works |
36+
| plain word without host or scheme returns null | Single word with no scheme → `null` |
37+
| relative path returns null | `some/relative/path` → `null` |
38+
| random clipboard text returns null | Plain prose → `null` |
39+
| dot-only string returns null | `.` → `null` |
40+
| file scheme URL is accepted | `file:///` local path is accepted |
41+
42+
---
43+
44+
## `git-core` — Git Data Models
45+
46+
**File:** `git-core/src/test/java/com/itsaky/androidide/git/core/GitModelsTest.kt`
47+
48+
Tests the data classes used to represent Git state: `GitStatus`, `FileChange`, `GitBranch`, `GitCommit`, and `CommitHistoryUiState`.
49+
50+
| Test | What it checks |
51+
|------|----------------|
52+
| testGitStatusEmpty | `GitStatus.EMPTY` is clean, has no conflicts, and all lists are empty |
53+
| GitStatus EMPTY is not merging | `isMerging` is false on the empty sentinel |
54+
| GitStatus with staged files is not clean | `isClean = false` when staged list is non-empty |
55+
| GitStatus with conflicts sets both flags | `hasConflicts` and `isMerging` are both true when a conflict exists |
56+
| GitStatus equality is value-based | Two equivalent `GitStatus` instances compare equal |
57+
| testFileChange | Basic path, type, and null `oldPath` on a MODIFIED change |
58+
| FileChange with ADDED type | `ChangeType.ADDED` is set; `oldPath` is null |
59+
| FileChange with DELETED type | `ChangeType.DELETED` is set |
60+
| FileChange with RENAMED type carries oldPath | `oldPath` is populated for a rename |
61+
| FileChange equality is value-based | Two equivalent `FileChange` instances compare equal |
62+
| FileChange with different paths are not equal | Different paths produce unequal instances |
63+
| testGitBranch | Local branch has correct name, `isCurrent`, and is not remote |
64+
| GitBranch remote branch carries remoteName | Remote branch stores `remoteName` |
65+
| GitBranch local branch has null remoteName by default | No `remoteName` on a local branch |
66+
| GitBranch equality is value-based | Two equivalent `GitBranch` instances compare equal |
67+
| testGitCommit | Hash, shortHash, and message are stored correctly |
68+
| GitCommit with parent hashes for merge commit | Merge commit stores two parent hashes and `hasBeenPushed` |
69+
| GitCommit equality is value-based | Two equivalent `GitCommit` instances compare equal |
70+
| GitCommit with different hashes are not equal | Different hashes produce unequal instances |
71+
| CommitHistoryUiState Loading is the loading state | `Loading` singleton is an instance of the Loading subtype |
72+
| CommitHistoryUiState Empty has no commits | `Empty` singleton is an instance of the Empty subtype |
73+
| CommitHistoryUiState Success carries commit list | `Success` holds and returns the provided commit list |
74+
| CommitHistoryUiState Error carries message | `Error` stores the provided error message string |
75+
| CommitHistoryUiState Error with null message | `Error` accepts a null message |
76+
| CommitHistoryUiState Success equality is value-based | Two `Success` instances with the same commits compare equal |
77+
78+
---
79+
80+
## `agent` — LLM Tool Call Parsing
81+
82+
**File:** `agent/src/test/java/com/itsaky/androidide/agent/repository/UtilParseToolCallTest.kt`
83+
84+
Tests `Util.parseToolCall()`, which extracts a structured tool call (name + args) from the raw text of an LLM response. The LLM may format the call in several ways.
85+
86+
| Test | What it checks |
87+
|------|----------------|
88+
| parses tool call with name and empty args from tool_call tags | JSON inside `<tool_call>` tags with no args is parsed |
89+
| parses tool call with args from tool_call tags | JSON inside `<tool_call>` tags with a file path arg is parsed |
90+
| parses tool call for list_files with path arg | `list_files` tool with `path` arg is parsed |
91+
| parses tool call for search_project | `search_project` tool with `query` arg is parsed |
92+
| parses bare JSON object with name field | JSON without any wrapping tags is parsed |
93+
| parses JSON wrapped in markdown code fence | ` ```json … ``` ` fence is stripped and parsed |
94+
| parses JSON wrapped in markdown code fence without json label | Plain ` ``` ` fence (no language label) is also handled |
95+
| list_dir is resolved to list_files | Alias `list_dir` maps to the canonical `list_files` tool name |
96+
| accepts tool_name field as alternative to name | `tool_name` key is accepted as a synonym for `name` |
97+
| parses tool-only tag with no args | `<tool_call>get_current_datetime</tool_call>` (no JSON) is parsed |
98+
| returns null when tool name is not in available tools | Unknown tool name → `null` |
99+
| returns null when name field is missing | JSON with no `name` key → `null` |
100+
| returns null when name field is blank | `name: ""` → `null` |
101+
| returns null for empty string | Empty input → `null` |
102+
| returns null for plain prose | Natural language sentence → `null` |
103+
| returns null when no tools are provided and response has a tool call | Empty tool set → always `null` |
104+
| extracts first JSON object and ignores trailing text | Prose after the JSON object is ignored |
105+
| parses multiple args correctly | Two args (`path`, `content`) are both extracted |
106+
| returns null when tool set is empty and input has JSON | Redundant empty-set check |
107+
108+
---
109+
110+
## `agent` — Chat Transcript Export
111+
112+
**File:** `agent/src/test/java/com/itsaky/androidide/agent/utils/ChatTranscriptUtilsTest.kt`
113+
114+
Tests `ChatTranscriptUtils.writeTranscriptToCache()`, which saves a chat session to a timestamped `.txt` file under the app's cache directory.
115+
116+
| Test | What it checks |
117+
|------|----------------|
118+
| writeTranscriptToCache creates file in chat_exports subdirectory | Output file lives inside a `chat_exports/` folder under `cacheDir` |
119+
| writeTranscriptToCache writes content verbatim | File content exactly matches the transcript string passed in |
120+
| writeTranscriptToCache filename starts with chat-transcript | Filename has the expected prefix |
121+
| writeTranscriptToCache filename ends with txt | Filename has the `.txt` extension |
122+
| writeTranscriptToCache two calls produce distinct filenames | Successive calls generate different filenames (timestamp-based) |
123+
| writeTranscriptToCache handles empty transcript | Empty string is written successfully; file is created |
124+
| writeTranscriptToCache handles multiline unicode content | Japanese, Arabic, and Chinese characters round-trip correctly |
125+
| writeTranscriptToCache throws when exports path is a file not a directory | `IOException` with "not a directory" message when `chat_exports` is a file |
126+
| writeTranscriptToCache re-uses existing exports directory | Pre-existing `chat_exports/` directory is reused, not duplicated |
127+
128+
---
129+
130+
## `common` — Keyed Debouncing Action
131+
132+
**File:** `common/src/test/java/com/itsaky/androidide/utils/KeyedDebouncingActionTest.kt`
133+
134+
Tests `KeyedDebouncingAction`, which debounces repeated work by key — rapid schedules for the same key are coalesced into a single action invocation, fired after a quiet period.
135+
136+
| Test | What it checks |
137+
|------|----------------|
138+
| single schedule triggers action exactly once | One `schedule()` call fires the action once after the debounce window |
139+
| rapid schedules are coalesced into a single action | Ten rapid `schedule("k")` calls result in exactly one action execution |
140+
| late reschedule of same key extends debounce window and fires exactly once | A second schedule mid-window resets the timer; action still fires only once |
141+
| different keys are handled independently | Scheduling `"alpha"` and `"beta"` fires both actions independently |
142+
| rapid schedules for different keys each trigger exactly once | Five sends for `"a"` and five for `"b"` each coalesce to one execution |
143+
| cancelPending prevents scheduled action from running | `cancelPending()` before the window expires suppresses the action |
144+
| cancelAll cancels all pending entries | `cancelAll()` with multiple keys pending results in zero executions |
145+
| cancelPending for unknown key is a no-op | Cancelling a key that was never scheduled does not throw or affect other keys |
146+
| can re-schedule a key after its action completes | A key can be scheduled again after its first action finishes |
147+
| action receives a cancel checker that is active while running | The `ICancelChecker` passed to the action reports not-cancelled during execution |
148+
149+
---
150+
151+
## `app` — Diagnostics Formatter
152+
153+
**File:** `app/src/test/java/com/itsaky/androidide/utils/DiagnosticsFormatterTest.kt`
154+
155+
Tests `DiagnosticsFormatter.format()`, which converts a map of `File → List<DiagnosticItem>` into a human-readable diagnostics report string.
156+
157+
| Test | What it checks |
158+
|------|----------------|
159+
| empty map returns no diagnostics message | `emptyMap()` input produces the literal string `"No diagnostics"` |
160+
| non-empty map contains header and summary | Output always includes `=== Diagnostics Report ===` and `=== Summary ===` sections |
161+
| file header shows correct error and warning counts | Per-file header shows `(N errors, M warnings)` |
162+
| file with only warnings shows zero errors | `0 errors` is shown when no errors exist for a file |
163+
| file with only errors shows zero warnings | `0 warnings` is shown when no warnings exist for a file |
164+
| ERROR severity label is written | `[ERROR]` appears in output for error-severity items |
165+
| WARNING severity label is written | `[WARNING]` appears in output |
166+
| INFO severity label is written | `[INFO]` appears in output |
167+
| HINT severity label is written | `[HINT]` appears in output |
168+
| line and column are reported as 1-indexed | 0-based `line=4, col=9` is displayed as `Line 5:10` |
169+
| zero-based line 0 col 0 reports as Line 1 col 1 | First position `(0,0)` displays as `Line 1:1` |
170+
| diagnostic message text is present in output | The diagnostic message string appears verbatim in the report |
171+
| diagnostic code is shown when present | Non-blank `code` field appears as `Code: E001` |
172+
| diagnostic code is omitted when blank | Blank `code` field does not produce a `Code:` line |
173+
| files are sorted alphabetically by name | Files appear in alphabetical order in the report |
174+
| diagnostics within a file are sorted by line number | Items within a file appear in ascending line-number order |
175+
| summary counts aggregate across all files | Summary section totals errors and warnings across all files |
176+
| summary files count only files with items | Files with empty diagnostic lists are excluded from the file count |
177+
| files with empty diagnostic list are not included in body | A file key with an empty list produces no output in the report body |
178+
179+
---
180+
181+
## `lsp:indexing` — Background Indexer
182+
183+
**File:** `lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/BackgroundIndexerTest.kt`
184+
185+
Tests `BackgroundIndexer`, which indexes source files into an `InMemoryIndex` asynchronously via coroutine jobs and reports progress via a listener.
186+
187+
| Test | What it checks |
188+
|------|----------------|
189+
| indexSource inserts entries into backing index | Entries from a provider are written to the index after the job completes |
190+
| indexSource skips already-indexed source when skipIfExists is true | Provider is never called when the source is already in the index |
191+
| indexSource re-indexes source when skipIfExists is false | Old entries are removed and new entries are inserted when force-reindexing |
192+
| progress listener receives Started and Completed events | Listener receives `Started` then `Completed` for a normal indexing run |
193+
| progress listener Completed event carries total count | `Completed.totalIndexed` equals the number of entries provided |
194+
| progress listener receives Skipped when source already indexed | Listener receives `Skipped` when `skipIfExists = true` and source exists |
195+
| progress listener receives Failed event on provider exception | Listener receives `Failed` when the provider sequence throws |
196+
| indexSources indexes all provided sources | Multiple sources are all indexed, each queryable by `bySource()` |
197+
| activeJobCount reflects running jobs | Count is positive while a job is in progress and zero after completion |
198+
| awaitAll suspends until all active jobs complete | `awaitAll()` only returns once every active indexing job has finished |
199+
200+
---
201+
202+
## `lsp:indexing` — Index Query Builder
203+
204+
**File:** `lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/IndexQueryBuilderTest.kt`
205+
206+
Tests the `indexQuery { }` DSL and `IndexQuery` static factories, which construct query objects used to filter entries in the symbol index.
207+
208+
| Test | What it checks |
209+
|------|----------------|
210+
| empty builder produces ALL-equivalent query | An empty `indexQuery {}` block has no predicates and the default limit of 200 |
211+
| eq adds exact match predicate | `eq("kind", "class")` adds a field/value pair to `exactMatch` |
212+
| multiple eq calls accumulate | Multiple `eq()` calls each add their own entry; none overwrite the others |
213+
| prefix adds prefix match predicate | `prefix("name", "Array")` adds an entry to `prefixMatch` |
214+
| multiple prefix calls accumulate | Multiple `prefix()` calls each add their own entry |
215+
| exists sets presence predicate to true | `exists("field")` adds `field → true` to `presence` |
216+
| notExists sets presence predicate to false | `notExists("field")` adds `field → false` to `presence` |
217+
| sourceId is set via builder property | `sourceId = "my-jar.jar"` is reflected in the built query |
218+
| key is set via builder property | `key = "com.example.Foo"` is reflected in the built query |
219+
| limit can be overridden | `limit = 50` is reflected in the built query |
220+
| limit zero means unlimited | `limit = 0` is reflected as zero (no cap) in the built query |
221+
| combined predicates all appear in built query | All predicate types together produce a single coherent query |
222+
| IndexQuery ALL has no predicates and default limit | The `ALL` factory produces an unconstrained query |
223+
| IndexQuery byKey sets key and limit 1 | `byKey()` factory sets the key and caps results to 1 |
224+
| IndexQuery bySource sets sourceId and unlimited limit | `bySource()` factory sets the source and uses unlimited (0) limit |
225+
| modifying builder after build does not affect built query | Post-build mutation of the builder does not change the already-built query |
226+
| two queries with identical predicates are equal | Data class equality holds for equivalent queries |
227+
| queries with different predicates are not equal | Queries with differing predicate values are not equal |

0 commit comments

Comments
 (0)