|
| 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