diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml new file mode 100644 index 0000000..74a685d --- /dev/null +++ b/.github/workflows/test-coverage.yaml @@ -0,0 +1,57 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: test-coverage.yaml + +permissions: read-all + +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v6 + + - uses: r-lib/actions/setup-r@v2 + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr, any::xml2 + needs: coverage + + - name: Test coverage + run: | + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + covr::to_cobertura(cov) + shell: Rscript {0} + + - uses: codecov/codecov-action@v7 + with: + fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} + files: ./cobertura.xml + plugins: noop + disable_search: true + + - name: Show testthat output + if: always() + run: | + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/DESCRIPTION b/DESCRIPTION index dc0124c..19ae481 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: codriver Title: Context-Aware AI Assistant for 'RStudio' -Version: 1.0.0 +Version: 1.0.0.9000 Authors@R: person("S.A.", "van der Wulp", , "vdwulp@gmail.com", role = c("aut", "cre", "cph")) Description: A context-aware AI assistant for 'RStudio' that works directly in the source editor without switching context or opening a separate chat diff --git a/NEWS.md b/NEWS.md index 0f8cb25..244f263 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # codriver (development version) +* Increased test coverage to 100%. + # codriver 1.0.0 * Initial CRAN submission. diff --git a/R/zzz.R b/R/zzz.R index 117c3cc..ecc0a3b 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -3,9 +3,13 @@ #' @noRd .onLoad <- function(libname, pkgname) { + # nocov start - no test coverage for this function + # Create a private environment each time the package is loaded .codriver_env <<- new.env(parent = emptyenv()) # Initialize config slot .codriver_env$config <- read_config() + + # nocov end } diff --git a/README.md b/README.md index ffbd414..b6b65e2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![CRAN status](https://www.r-pkg.org/badges/version/codriver)](https://CRAN.R-project.org/package=codriver) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/vdwulp/codriver/blob/master/LICENSE.md) [![R-CMD-check](https://github.com/vdwulp/codriver/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/vdwulp/codriver/actions/workflows/R-CMD-check.yaml) +[![Codecov test coverage](https://codecov.io/gh/vdwulp/codriver/branch/main/graph/badge.svg)](https://app.codecov.io/gh/vdwulp/codriver/tree/main) *A context-aware AI assistant for RStudio that is literally at your fingertips.* diff --git a/tests/testthat/test-build_prompt.R b/tests/testthat/test-build_prompt.R index ad2928b..10aa25c 100644 --- a/tests/testthat/test-build_prompt.R +++ b/tests/testthat/test-build_prompt.R @@ -352,6 +352,33 @@ test_that("build_prompt - preamble reports no library calls when none present", }) + +# ── Line truncation ─────────────────────────────────────────────────────────── + +test_that("build_prompt - before: truncates to n_lines from end when context exceeds limit", { + # 35 lines > 30-line limit; only last 30 should appear in Code window + ctx <- make_context(c(paste0("line", seq_len(35L)), "")) + action <- make_action("continue", position = pos(36L, 1L)) + bp_mocks({ + result <- bp(ctx, action) + expect_match(result$user, "line35") + expect_false(grepl("line1\\b", result$user)) + }) +}) + +test_that("build_prompt - after: truncates to n_lines from start when context exceeds limit", { + # position at line 1; 15 lines after > 10-line limit for after() + ctx <- make_context(c("x <- 1", paste0("after", seq_len(15L)))) + action <- make_action("edit", text = "x <- 1", position = pos(1L, 7L), + range = rng(1L, 1L, 1L, 7L)) + bp_mocks({ + result <- bp(ctx, action) + expect_match(result$user, "after1") + expect_false(grepl("after15", result$user)) + }) +}) + + # ── Block formatting ────────────────────────────────────────────────────────── test_that("build_prompt - blocks use START/END delimiters", { diff --git a/tests/testthat/test-build_prompt_workspace.R b/tests/testthat/test-build_prompt_workspace.R index b40e522..a5d07c7 100644 --- a/tests/testthat/test-build_prompt_workspace.R +++ b/tests/testthat/test-build_prompt_workspace.R @@ -329,7 +329,7 @@ test_that("describe_columns - detects suffix pattern when columns share a last t test_that("describe_columns - suffix owned by a single prefix group is discarded", { # All _usd columns start with "amt" -> single prefix owner -> discard - result <- dc(c("amt_usd_a", "amt_usd_b", "other_eur")) + result <- dc(c("amt_a_usd", "amt_b_usd", "other_eur")) expect_false(grepl("\\*_usd", result)) }) diff --git a/tests/testthat/test-configure.R b/tests/testthat/test-configure.R index 1e8c5e9..c8dd23a 100644 --- a/tests/testthat/test-configure.R +++ b/tests/testthat/test-configure.R @@ -308,3 +308,26 @@ test_that("is_shortcut_registered - returns empty string when codriver::codriver local_mocked_bindings(rstudio_config_path = function(...) tmp, .package = "codriver") expect_equal(is_shortcut_registered(), "") }) + + +# ── register_shortcut ───────────────────────────────────────────────────────── + +test_that("register_shortcut - returns TRUE when shortcut is registered after call", { + with_mocked_bindings( + use_rstudio_keyboard_shortcut = function(...) invisible(NULL), + is_shortcut_registered = function() "Ctrl+Shift+P", + { + expect_true(register_shortcut("Ctrl+Shift+P")) + } + ) +}) + +test_that("register_shortcut - returns FALSE when shortcut is not registered after call", { + with_mocked_bindings( + use_rstudio_keyboard_shortcut = function(...) invisible(NULL), + is_shortcut_registered = function() NULL, + { + expect_false(register_shortcut("Ctrl+Shift+P")) + } + ) +}) diff --git a/tests/testthat/test-resolve_action.R b/tests/testthat/test-resolve_action.R index cbd15aa..9707f00 100644 --- a/tests/testthat/test-resolve_action.R +++ b/tests/testthat/test-resolve_action.R @@ -99,6 +99,10 @@ test_that("scan_lines - ignores # inside raw string with padding r\"-(...)- \"", expect_equal(scan_col(c('x <- r"-(# not a comment)-"'), 1L), -1L) }) +test_that("scan_lines - ignores # inside raw string with multi-dash padding", { + expect_equal(scan_col(c('x <- r"---(# not a comment)---"'), 1L), -1L) +}) + test_that("scan_lines - detects comment after closed raw string", { expect_equal(scan_col(c('x <- r"(text)" # comment'), 1L), 16L) }) @@ -345,6 +349,12 @@ test_that("resolve_action - multi-line selection ending at col 1 trims trailing expect_false(endsWith(result$text, "\n")) }) +test_that("resolve_action - multi-line code selection where anchor is start row with inline comment -> comment", { + ctx <- make_selection_context(c("x <- 1 # old", " "), 1L, 8L, 2L, 2L) + result <- ra(ctx) + expect_equal(result$mode, "comment") +}) + test_that("resolve_action - multi-line comment selection -> generate from contiguous block", { ctx <- make_selection_context(c("# line one", "# line two", "x <- 1"), 1L, 1L, 2L, 11L) result <- ra(ctx) @@ -352,6 +362,15 @@ test_that("resolve_action - multi-line comment selection -> generate from contig expect_equal(result$text, "line one\nline two") }) +test_that("resolve_action - multi-line comment selection with non-comment row above anchor stops block at code row", { + # row 1 is code, rows 2-3 are comments; selection covers all three + # anchor = row 3 (comment), walk up hits row 1 (code) -> break -> block is rows 2-3 only + ctx <- make_selection_context(c("x <- 1", "# line one", "# line two"), 1L, 1L, 3L, 11L) + result <- ra(ctx) + expect_equal(result$mode, "generate") + expect_equal(result$text, "line one\nline two") +}) + test_that("resolve_action - multi-line selection with only whitespace -> continue", { ctx <- make_selection_context(c("", ""), 1L, 1L, 2L, 1L) result <- ra(ctx) @@ -362,6 +381,32 @@ test_that("resolve_action - multi-line whitespace selection with comment start l ctx <- make_selection_context(c("# instruction", " ", " "), 1L, 1L, 3L, 1L) result <- ra(ctx) expect_equal(result$mode, "generate") + expect_equal(result$text, "instruction") +}) + +test_that("resolve_action - multi-line whitespace selection with comment start line, anchor in whitespace -> generate", { + ctx <- make_selection_context(c("# instruction ", " "), 1L, 15L, 2L, 2L) + result <- ra(ctx) + expect_equal(result$mode, "generate") + expect_equal(result$text, "instruction") +}) + +test_that("resolve_action - multi-line whitespace selection with code start line, anchor at comment column -> comment", { + ctx <- make_selection_context(c("x <- 1 # note", " "), 1L, 8L, 2L, 2L) + result <- ra(ctx) + expect_equal(result$mode, "comment") +}) + +test_that("resolve_action - multi-line whitespace selection with code start line, anchor past inline comment -> comment", { + ctx <- make_selection_context(c("x <- 1 # note ", " "), 1L, 15L, 2L, 2L) + result <- ra(ctx) + expect_equal(result$mode, "comment") +}) + +test_that("resolve_action - multi-line whitespace selection with code start line -> complete", { + ctx <- make_selection_context(c("x <- 1 ", " "), 1L, 8L, 2L, 2L) + result <- ra(ctx) + expect_equal(result$mode, "complete") })