test/ai-coverage-unit-only #118
Closed
jabber.developer2
wants to merge 0 commits from
test/ai-coverage-unit-only into feat/ai
pull from: test/ai-coverage-unit-only
merge into: devs:feat/ai
devs:master
devs:feat/ai-api-type
devs:fix/editor-terminal-size
devs:ci/docker-hub-publishing
devs:feat/disco-ac
devs:feat/privacy-enhancements
devs:fix/clientid-regression
devs:fix/ai-chat-completions-followup
devs:feat/ai-custom
devs:fix/unencrypted-send
devs:rollback/pre-upstream-merge
devs:fix/autoping-warning-null-domain
devs:fix/pad-dead-space-reclaim
devs:feat/autoping-warning
devs:chore/untrack-gitversion
devs:fix/multiline-pad-clip
devs:fix/issue-112-followups
devs:fix/issue-128-migrate-v3-dedup
devs:fix/delay-timestamp-validation
devs:ref/light-cleanup
devs:fix/history-scroll-pad-redraw-storm
devs:fix/plugin-post-display-incoming-only
devs:merge/upstream-full
devs:merge-improve
devs:chore/remove-chatlog-stage-1
devs:fix/ai-json-encoding
devs:feat/no-db-backlog-114
devs:fix/scroll-non-chat-windows
devs:fix/paged-non-chat-windows
devs:fix/ai-leaks
devs:feat/ai
devs:fix/ai-followups
devs:test/ai-coverage
devs:refactor/scroll-mechanism
devs:fix/verify-per-contact-context
devs:feat/no-db-mode
devs:feat/ai-json
devs:feat/pikaur-parity-arch
devs:fix-pikaur-build
devs:feat/upstream-sync
devs:feat/functest-speedup
devs:fix/cwe-134-format-string-audit
devs:ci/separate-build-step
devs:test/autoping-functional-tests
devs:test/db-functional-tests
devs:fix/arch-build
devs:fix/xep-0030-disco-items-error-handling
devs:fix/xep-0030-empty-disco-items
devs:playground/fix/src_refactoring
devs:tests/disco
devs:fix/test-CI-stability
devs:feat/parallel-tests-clean
devs:feat/parallel-functional-tests
devs:fix/functional_tests_v2
devs:fix/functional_tests
devs:fix/connect_max_args
devs:feat/extended_debug_info
devs:playground/fix/scroll-stuck
devs:build/multicore
devs:build/reenable-fedora
devs:build/autoupdate
No Reviewers
Labels
Clear labels
Compat/Breaking
Breaking change that won't be backward compatible
Kind/Bug
Something is not working
Kind/Documentation
Documentation changes
Kind/Enhancement
Improve existing functionality
Kind/Feature
New functionality
Kind/Security
This is security issue
Kind/Testing
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: devs/cproof#118
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
No description provided.
Delete Branch "test/ai-coverage-unit-only"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
test(ai): expand /ai test coverage + fix two autocomplete bugs
Base:
feat/ai(HEAD010b2062a)Head:
test/ai-coverage-unit-only— 3 commitsWhat's in this PR
The first two commits add coverage and surface two real autocomplete bugs through failing tests. The third commit fixes the bugs, turning all tests green.
Commit 1 —
test(ai): expand unit and functional coverage for /ai featureUnit tests (
tests/unittests/test_ai_client.c): 50 → 100 cases.ai_parse_response): OpenAIcontent+ Perplexitytextformats, escape decoding, empty/null/missing inputs, format-string safety, multiline content.ai_parse_error_message): standard envelope, nested escapes, missing fields, null/empty.\b,\f,\r, all-specials, UTF-8 pass-through.set_model(NULL),ref/unref(NULL).get/removewith NULL, double-remove, survival via session ref afterai_remove_provider.datanot array, empty data,"id"outside data, multiple models.set token→shutdown→init→ token reloaded from disk (usesload_preferencesfixture).Functional tests (
tests/functionaltests/test_ai.c): 15 new cases for the/aicommand surface that don't need HTTP. Covers/ai,/ai providers [list],/ai set provider/token/default-*,/ai start(with/without key/unknown provider),/ai clear,/ai remove provider,/ai switch,/ai bad-subcmd.Infrastructure:
TEST_GROUPSbumped to 5 in proftest.c. AI tests live in their own Group 5 because mixing them with stabber-driven tests in Group 4 hangsstbbr_stop()at teardown — see "Side issue" below.PROF_FUNC_TEST_AImacro in functionaltests.c registers AI tests withai_init_test()which wrapsinit_prof_test+prof_connect()so stabber sees a graceful disconnect at teardown.Source-level changes to enable testing (
src/ai/ai_client.{c,h}):staticfrom_parse_ai_response→ai_parse_response, and_parse_error_response→ai_parse_error_message. Same approach already used byai_parse_models_from_json. Declared in the header under a "Parsing helpers (exposed for testing)" section.Commit 2 —
test(ai): autocomplete tests — 5 expected failures documenting real bugs10 new tests for
ai_models_findand additionalai_providers_findedge cases. At this commit, 5 pass as sanity and 5 fail intentionally because they catch two distinct production bugs.The failing tests were deliberately committed as red so the bug is reproducible and traceable in git history. They turn green in commit 3.
Commit 3 —
fix(ai): autocomplete cycling and reset hookFixes both bugs caught in commit 2. After this commit, all 605 unit tests + 15 Group 5 functional tests pass.
🐞 Bugs and fixes
Bug A —
ai_models_findcycling is completely brokenBefore (src/ai/ai_client.c
ai_models_find()):Each call allocated a new
Autocompleteand freed it before returning. Thelast_foundcursor thatautocomplete_completeuses to advance through cycle positions was therefore never persisted between calls. Repeated calls with the same prefix always returned the first match. Tab+Tab+Tab could never reach models 2, 3, ….Caught by:
test_ai_models_find_cycles_through_matchestest_ai_models_find_null_search_cycles_allFix: turn
models_acinto a static, rebuild only when the underlying provider object or its model count changes:The
models_ac_models_leninvariant catches the/ai models <p>refresh case where the provider pointer is unchanged but the model list was rebuilt by_parse_and_cache_models. This avoids touching the cache-update path itself.Bug B —
providers_acis not reset between user input cyclessrc/command/cmd_ac.c:1655 —
cmd_ac_reset()iteratesall_acs[]and callsautocomplete_reset(*ac)on each.all_acs[](defined incmd_ac.c) contains&ai_subcommands_ac,&ai_set_subcommands_ac,&ai_remove_subcommands_ac, but not&providers_ac— that's static inai_client.cand never registered.Consequence: when the input loop fired the global reset, every other AC got its
last_foundcursor wiped — exceptproviders_ac. So when the user started a new completion,autocomplete_completefound a stalelast_foundand continued from there rather than restarting at the head of the new prefix's matches.User-visible symptoms:
/ai start ope(gotopenai), the user backspaces and types/ai start per— instead ofperplexity, the cycle continued from the previous cursor position, possibly missing matches near the head of the list./ai set provider ...and then trying to tab-complete it could also miss the new entry depending on where the leftover cursor sat.Caught by:
test_ai_providers_find_state_resets_on_prefix_changetest_ai_providers_find_empty_then_prefixtest_ai_providers_find_after_add_includes_newFix: expose
ai_autocomplete_reset()fromai_client.{c,h}and call it fromcmd_ac_reset()alongsidebookmark_autocomplete_reset()/blocked_ac_reset()/ etc.Additional defensive change:
ai_providers_findandai_models_findnow detect asearch_strchange between consecutive calls (via cached*_ac_last_searchstrings) and reset the cycling cursor themselves. This means callers that drive the function directly — unit tests, future scripted clients — get correct cycling without having to know about theai_autocomplete_reset()hook. In interactive use,cmd_ac_reset()still fires on every input cycle and gives the same effect from the outside.This goes slightly beyond the minimum needed for Bug B (the input-loop reset alone would have been enough for the PTY path), but the four extra lines remove a foot-gun for any non-input-loop caller.
Sanity tests added (already passing in commit 2)
These document expected behavior and guard against future regressions:
test_ai_models_find_no_models_returns_nullNULL, no crashtest_ai_models_find_returns_match_for_prefix"o1"on a 3-model set returns"o1-preview"test_ai_models_find_no_match_returns_nullNULLtest_ai_models_find_previous_directionprevious=TRUEreturns non-NULL when matches existtest_ai_providers_find_after_remove_skips_removedai_remove_provider, completion no longer offers itSide issue: stabber upstream hangs in
stbbr_stop()when no client ever connectedNot a profanity bug, but discovered while wiring the AI functional tests and worth flagging. Stabber's server thread is parked in
accept()until a connection arrives;stbbr_stop()callspthread_joinon that thread, which blocks forever. Profanity's standard functional tests don't hit this becauseprof_connect()runs in nearly every test, so the listener has seen a client by teardown time.AI tests don't need XMPP, so the workaround in
ai_init_test()callsprof_connect()anyway. Documented in test_ai.c and purely a test-side workaround. The underlying fix belongs upstream in stabber —stbbr_stop()shouldshutdown(listen_fd, SHUT_RDWR)before joining soaccept()returns.Test results in Docker (cproof-debian image)
Out of scope (intentionally)
test/ai-coverage) which adds a Python HTTP stub. Held back because the stub-server infrastructure is a bigger commitment and warrants its own review.N1/N2/X1/X2/X4from PR 113 review). These need TSan or carefully timed race scenarios thatcmocka+forkptycan't reproduce.Test plan for reviewer
make check— all 605 unit tests pass, no failures../tests/functionaltests/functionaltests 5— 15 Group 5 tests pass.git log --oneline feat/ai..HEAD— three commits, layered so the bug-catching commit (red) and the fix commit (green) are separable for git-bisect / cherry-pick./ai set provider new-name <url>is picked up by subsequent<Tab>cycles.Unit tests (tests/unittests/test_ai_client.c): +50 cases on top of the existing 50 — total 100. Covers: - chat response parser (ai_parse_response): OpenAI content + Perplexity text formats, escape decoding, empty/null/missing inputs, format-string safety, multiline content - error envelope parser (ai_parse_error_message): standard envelope, nested escapes, missing fields, null/empty - extended JSON escape: \b, \f, \r, all-specials, UTF-8 pass-through - provider autocomplete cycling with >=2 matches and wrap-around - session edge cases: NULL args, 100-message order preservation, set_model(NULL), ref/unref(NULL) - provider edge cases: get/remove with NULL, double-remove, survival via session ref after ai_remove_provider - settings: multi-key independence, missing key, cross-provider isolation - model parsing edges: data not array, empty data, "id" outside data, multiple models - prefs round-trip: set token -> shutdown -> init -> token reloaded from disk (uses load_preferences fixture) Functional tests: - Console only (test_ai.c): 15 cases for the /ai command surface that don't need HTTP. Covers /ai help, providers list, set provider/token, start with/without key/unknown provider, clear, remove, default provider/model, switch without window, bad subcommand. Infrastructure: - TEST_GROUPS bumped to 5 in proftest.c; AI tests live in their own Group 5 because mixing them with stabber-driven tests in Group 4 poisoned stbbr_stop() teardown. - PROF_FUNC_TEST_AI macro in functionaltests.c registers AI tests with ai_init_test() which wraps init_prof_test with a prof_connect() so stabber sees a graceful disconnect at teardown. Source-level changes to enable testing: - src/ai/ai_client.{c,h}: dropped 'static' from _parse_ai_response (renamed ai_parse_response) and _parse_error_response (renamed ai_parse_error_message); declared under a "Parsing helpers (exposed for testing)" section. Same approach as ai_parse_models_from_json. Results in Docker (cproof-debian image): - 595/595 unit tests pass - 15/15 AI functional tests pass (Group 5)Adds 10 tests for ai_models_find and additional edge cases on ai_providers_find. 5 pass as sanity; 5 fail intentionally, documenting two distinct bugs in the autocomplete integration: BUG A — ai_models_find cycling is broken src/ai/ai_client.c ai_models_find() allocates a fresh Autocomplete on every call and frees it before returning. The Autocomplete's last_found cursor is therefore never persisted, so repeated calls with the same prefix always return the first match. Tab+Tab+Tab cannot reach models 2, 3, ... Caught by: - test_ai_models_find_cycles_through_matches - test_ai_models_find_null_search_cycles_all BUG B — providers_ac is never reset between user input cycles src/ai/ai_client.c keeps the static providers_ac AC, but src/command/cmd_ac.c's all_acs[] (which cmd_ac_reset() iterates) does not include &providers_ac. So when the input loop fires the global reset, providers_ac retains its last_found cursor from the previous prefix. Result: tab-completing a new prefix continues from the old cursor position instead of restarting at the head of the new prefix's matches. A provider added mid-session may also be missed depending on cursor position. Caught by: - test_ai_providers_find_state_resets_on_prefix_change - test_ai_providers_find_empty_then_prefix - test_ai_providers_find_after_add_includes_new Sanity (5 passing): - test_ai_models_find_no_models_returns_null - test_ai_models_find_returns_match_for_prefix - test_ai_models_find_no_match_returns_null - test_ai_models_find_previous_direction - test_ai_providers_find_after_remove_skips_removed The failing tests are checked in as-is — they go green when the bugs are fixed. See PR description for suggested fixes.3fccc046b0toaade5767b4aade5767b4tobefdd0ba104272977ab3to65c6012677Pull request closed