Skip to content

v0.4.0: Improve test coverage, fix bugs, and update CI#13

Merged
VincentGuyader merged 22 commits intomasterfrom
v4
Feb 13, 2026
Merged

v0.4.0: Improve test coverage, fix bugs, and update CI#13
VincentGuyader merged 22 commits intomasterfrom
v4

Conversation

@VincentGuyader
Copy link
Member

Summary

  • Add comprehensive tests for workspaces, activity, backups, templates, uploads modules
  • Add HTTP error handling with httr2 req_error()
  • Fix glue() to character conversion in templates.R
  • Remove debug message from call_api()
  • Extract API URLs as package constants
  • Update GitHub Actions to modern versions (v2/v4)
  • Add skip_if_test_project_missing() test helper
  • Add NEWS.md changelog
  • Bump version to 0.4.0

Test plan

  • All tests pass locally (0 FAIL, 144 PASS, 34 SKIP)
  • CI workflow runs successfully with updated actions
  • R CMD check passes on all platforms

- Add comprehensive tests for workspaces, activity, backups, templates, uploads
- Add HTTP error handling with httr2 req_error()
- Fix glue() to character conversion in templates.R
- Remove debug message from call_api()
- Extract API URLs as package constants
- Update GitHub Actions to modern versions (v2/v4)
- Add skip_if_test_project_missing() test helper
- Add NEWS.md changelog
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR releases v0.4.0 by expanding module coverage (workspaces/activity/backups/templates/uploads), improving HTTP error handling via httr2, modernizing CI, and updating package metadata/changelog.

Changes:

  • Add new API modules/functions (workspaces, activity, backups, templates, uploads, reminders, comments, extended user helpers) and expose them via NAMESPACE
  • Improve HTTP request handling (constants for base URLs; req_error() in core API helpers)
  • Update GitHub Actions workflow and add repository guidance/docs (NEWS, CLAUDE.md, build ignore updates)

Reviewed changes

Copilot reviewed 183 out of 330 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
R/utils.R Adds `%
R/workspaces.R Introduces workspace CRUD-like helpers via Sync API commands
R/users.R Adds user info/productivity helpers and invitation/collaborator management
R/uploads.R Adds upload + delete-upload helpers using REST endpoints
R/templates.R Adds template import/export helpers and fixes glue() URL coercion
R/section.R Adds section update/delete/move/archive/unarchive + REST section getters
R/reminders.R Adds reminders CRUD via Sync API commands + listing reminders
R/comments.R Adds comments CRUD + pagination-backed listing
R/activity.R Adds activity log retrieval and normalization into a data.frame
NEWS.md Adds v0.4.0 changelog entry
DESCRIPTION Bumps version to 0.4.0 and adjusts metadata/dependencies
NAMESPACE Exports new functions and imports additional httr2 helpers
.github/workflows/R-CMD-check.yaml Migrates to r-lib/actions v2 and simplifies matrix/dependency setup
.Rbuildignore Ignores Claude/plan files and check artifacts
CLAUDE.md Adds contributor/dev workflow & architecture notes
check/rtodoist.Rcheck/** Adds R CMD check output artifacts (installed tree, man pages, test snapshots, etc.)
Comments suppressed due to low confidence (4)

R/reminders.R:1

  • JSON for Sync API commands is being manually assembled with interpolated user-controlled strings (task_id, type, due_*). If any of these contain quotes/backslashes/newlines, the resulting JSON becomes invalid (or can be interpreted differently than intended). Suggested fix: either wrap all interpolated string values with escape_json() or (preferably) build an R list for args and serialize it with a JSON encoder (e.g., jsonlite) to avoid injection/escaping issues.
    R/workspaces.R:1
  • The Sync API commands JSON is assembled via glue() with raw workspace_id, email, and role values. These should be escaped to keep JSON valid and prevent malformed requests when values contain special characters. Suggested fix: apply escape_json() to each interpolated string field (or switch to building a list and JSON-encoding it).
    R/uploads.R:1
  • This introduces a hard-coded REST base URL even though TODOIST_REST_URL was added in R/utils.R. For consistency (and to avoid future drift if the API base changes), use the shared constants across all modules (uploads/comments/templates/etc.).
    R/comments.R:1
  • Core helpers (call_api*) now include req_error() for HTTP error handling, but this direct REST call path doesn’t. As a result, 4xx/5xx responses may be parsed as JSON (or fail later) without a clear error. Suggested fix: add req_error(...) (consistent with call_api_rest) before req_perform() or refactor to reuse call_api_rest() for REST endpoints to centralize error behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

VincentGuyader and others added 2 commits February 9, 2026 09:09
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Remove check/ directory (R CMD check artifact) and add to .gitignore
- Fix tryCatch() syntax in ask_todoist_api_token (interrupt = function(e) NULL)
- Add escape_json() for JSON string interpolation in reminders.R and workspaces.R
- Use TODOIST_REST_URL constant instead of hardcoded URLs in uploads.R and comments.R
- Add req_error() for proper HTTP error handling in comments.R
- Fix DESCRIPTION comment field formatting (named elements)
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 115 out of 116 changed files in this pull request and generated 21 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Add req_error() for HTTP error handling in:
  - templates.R (import_template, export_template)
  - labels.R (rename_shared_label, remove_shared_label)
  - uploads.R (delete_upload)
  - backups.R (download_backup)

- Use TODOIST_REST_URL constant instead of hardcoded URLs in:
  - templates.R
  - labels.R (shared label functions)

- Fix empty data.frame column consistency:
  - comments.R: add task_id and project_id columns
  - reminders.R: add due_date and minute_offset columns

- Remove unused project_name parameter from unarchive_project()

- Update NEWS.md to document all changes
- Use TODOIST_REST_URL constant in quick_add_task() instead of hardcoded URL
- Add req_error() to quick_add_task() and upload_file() for HTTP error handling
- Fix empty data.frame column consistency in:
  - get_activity_logs(): add initiator_id, parent_project_id, parent_item_id
  - get_tasks_by_filter(): add due_date
  - get_archived_projects(): add color, is_favorite
  - get_all_sections(): add order
  - get_all_workspaces(): add is_default
  - get_workspace_users(): add role
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 116 out of 117 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

R/workspaces.R Outdated
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "workspace_update", "uuid": "{random_key()}", "args": {{"id": "{workspace_id}", "name": "{escape_json(name)}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_workspace() builds the Sync API JSON command by interpolating workspace_id directly into the JSON string. If workspace_id contains quotes/backslashes, this will produce invalid JSON (and is inconsistent with invite_to_workspace() which uses escape_json() for IDs). Use escape_json(workspace_id) when constructing the command args (and similarly for other unescaped string fields).

Suggested change
commands = glue('[{{"type": "workspace_update", "uuid": "{random_key()}", "args": {{"id": "{workspace_id}", "name": "{escape_json(name)}"}}}}]')
commands = glue('[{{"type": "workspace_update", "uuid": "{random_key()}", "args": {{"id": "{escape_json(workspace_id)}", "name": "{escape_json(name)}"}}}}]')

Copilot uses AI. Check for mistakes.
Comment on lines +430 to +434
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "item_close", "uuid": "{random_key()}", "args": {{"id": "{task_id}"}}}}]')
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close_task() interpolates task_id directly into a JSON string for the Sync API command args. For robustness (and to avoid malformed JSON if task_id contains escapable characters), pass it through escape_json() or build the command payload with a JSON encoder.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +462 to +466
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "item_uncomplete", "uuid": "{random_key()}", "args": {{"id": "{task_id}"}}}}]')
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reopen_task() interpolates task_id directly into the JSON command string. Use escape_json(task_id) (or JSON encoding) to ensure the Sync API payload remains valid for all inputs.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

R/users.R Outdated
Comment on lines +317 to +320
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "collaborator_delete", "uuid": "{random_key()}", "args": {{"project_id": "{project_id}", "email": "{email}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_collaborator() interpolates email (and project_id) directly into the JSON command string without escaping. Since email is user-provided, this can break JSON and lead to malformed Sync API requests. Use escape_json() for these string fields (and consider encoding the whole args list to JSON rather than manual string assembly).

Suggested change
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "collaborator_delete", "uuid": "{random_key()}", "args": {{"project_id": "{project_id}", "email": "{email}"}}}}]')
commands_list <- list(list(
type = "collaborator_delete",
uuid = random_key(),
args = list(
project_id = project_id,
email = email
)
))
call_api(
token = token,
sync_token = "*",
commands = jsonlite::toJSON(commands_list, auto_unbox = TRUE)

Copilot uses AI. Check for mistakes.
Comment on lines +417 to +421
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "delete_invitation", "uuid": "{random_key()}", "args": {{"invitation_id": "{invitation_id}"}}}}]')
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_invitation() interpolates invitation_id directly into the JSON command string without escaping. Use escape_json(invitation_id) (or JSON encoding) to ensure the payload is always valid.

Copilot uses AI. Check for mistakes.
Comment on lines +398 to +402
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "item_delete", "uuid": "{random_key()}", "args": {{"id": "{task_id}"}}}}]')
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_task() interpolates task_id directly into a JSON string ("id": "{task_id}"). If task_id contains characters that need escaping, the generated JSON will be invalid. Use escape_json(task_id) (or a safer JSON builder) when constructing the Sync API command args.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

@VincentGuyader
Copy link
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #14, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #15, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 3 commits February 9, 2026 14:15
* Fix JSON escaping in Sync API commands across all modules
…ew (#14)

- Fix call_api() incorrect parameter (base_url -> url)
- Add verbose flag to add_label() existing label message
- Add escape_json() to all Sync API string interpolations
Apply escape_json() to all user-controlled values in JSON payloads:

- workspaces.R: escape workspace_id in update_workspace()
- tasks.R:
  - escape task_id in delete_task(), close_task(), reopen_task()
  - escape task_id and due_date in update_task()
  - escape labels array values in update_task()
- users.R:
  - escape project_id and email in delete_collaborator()
  - escape invitation_id and invitation_secret in accept_invitation()
  - escape invitation_id and invitation_secret in reject_invitation()
  - escape invitation_id in delete_invitation()

This prevents malformed JSON payloads when inputs contain quotes,
backslashes, or other special characters.
Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #16, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #17, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #18, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 116 out of 117 changed files in this pull request and generated 13 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

R/tasks.R Outdated
Comment on lines +496 to +505
args_parts <- c(glue('"id": "{task_id}"'))

if (!is.null(project_id)) {
args_parts <- c(args_parts, glue('"project_id": "{project_id}"'))
}
if (!is.null(section_id)) {
args_parts <- c(args_parts, glue('"section_id": "{section_id}"'))
}
if (!is.null(parent_id)) {
args_parts <- c(args_parts, glue('"parent_id": "{parent_id}"'))
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move_task() builds the Sync API command JSON by interpolating task_id, project_id, section_id, and parent_id without escape_json(). This is inconsistent with other commands in the package (e.g., item_close) and can produce invalid JSON if any of these values contain characters that must be escaped. Use escape_json() for all interpolated string values (including IDs) when constructing args_json (or build the JSON via a proper serializer).

Suggested change
args_parts <- c(glue('"id": "{task_id}"'))
if (!is.null(project_id)) {
args_parts <- c(args_parts, glue('"project_id": "{project_id}"'))
}
if (!is.null(section_id)) {
args_parts <- c(args_parts, glue('"section_id": "{section_id}"'))
}
if (!is.null(parent_id)) {
args_parts <- c(args_parts, glue('"parent_id": "{parent_id}"'))
args_parts <- c(glue('"id": "{escape_json(task_id)}"'))
if (!is.null(project_id)) {
args_parts <- c(args_parts, glue('"project_id": "{escape_json(project_id)}"'))
}
if (!is.null(section_id)) {
args_parts <- c(args_parts, glue('"section_id": "{escape_json(section_id)}"'))
}
if (!is.null(parent_id)) {
args_parts <- c(args_parts, glue('"parent_id": "{escape_json(parent_id)}"'))

Copilot uses AI. Check for mistakes.
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "section_update", "uuid": "{random_key()}", "args": {{"id": "{section_id}", "name": "{escape_json(new_name)}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_section() interpolates section_id directly into the JSON command without escape_json(). This can break the JSON payload for IDs containing escapable characters and is inconsistent with how other functions escape IDs (e.g., update_task). Escape section_id before interpolating it into the command.

Suggested change
commands = glue('[{{"type": "section_update", "uuid": "{random_key()}", "args": {{"id": "{section_id}", "name": "{escape_json(new_name)}"}}}}]')
commands = glue('[{{"type": "section_update", "uuid": "{random_key()}", "args": {{"id": "{escape_json(section_id)}", "name": "{escape_json(new_name)}"}}}}]')

Copilot uses AI. Check for mistakes.
R/section.R Outdated
Comment on lines +195 to +198
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "section_move", "uuid": "{random_key()}", "args": {{"id": "{section_id}", "project_id": "{project_id}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move_section() interpolates both section_id and project_id directly into the Sync API JSON command. These values should be passed through escape_json() to prevent malformed JSON and to match the escaping approach used elsewhere in the package.

Suggested change
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "section_move", "uuid": "{random_key()}", "args": {{"id": "{section_id}", "project_id": "{project_id}"}}}}]')
escaped_section_id <- escape_json(section_id)
escaped_project_id <- escape_json(project_id)
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "section_move", "uuid": "{random_key()}", "args": {{"id": "{escaped_section_id}", "project_id": "{escaped_project_id}"}}}}]')

Copilot uses AI. Check for mistakes.
R/projects.R Outdated
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "project_delete", "uuid": "{random_key()}", "args": {{"id": "{project_id}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_project() interpolates project_id into the Sync API JSON command without escape_json(), which can lead to malformed JSON if the ID ever contains escapable characters. Escape project_id here (and similarly in the archive/unarchive commands below) for consistency.

Suggested change
commands = glue('[{{"type": "project_delete", "uuid": "{random_key()}", "args": {{"id": "{project_id}"}}}}]')
commands = glue('[{{"type": "project_delete", "uuid": "{random_key()}", "args": {{"id": "{escape_json(project_id)}"}}}}]')

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +236
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "project_archive", "uuid": "{random_key()}", "args": {{"id": "{project_id}"}}}}]')
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

archive_project() inserts project_id into the JSON command without escape_json(). Use escape_json(project_id) to avoid malformed JSON and keep escaping consistent across Sync API commands.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +140 to +144
args_parts <- c(glue('"id": "{project_id}"'))

if (!is.null(new_name)) {
args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_project() builds a JSON fragment with project_id inserted without escape_json(). While IDs are usually simple strings, escaping here keeps the JSON generation robust and consistent with other command builders in this PR.

Copilot uses AI. Check for mistakes.
R/projects.R Outdated
call_api(
token = token,
sync_token = "*",
commands = glue('[{{"type": "project_unarchive", "uuid": "{random_key()}", "args": {{"id": "{project_id}"}}}}]')
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unarchive_project() interpolates project_id directly into the JSON command. Escape it with escape_json() (as done for other IDs in this PR) to keep command JSON robust to special characters.

Suggested change
commands = glue('[{{"type": "project_unarchive", "uuid": "{random_key()}", "args": {{"id": "{project_id}"}}}}]')
commands = glue('[{{"type": "project_unarchive", "uuid": "{random_key()}", "args": {{"id": "{escape_json(project_id)}"}}}}]')

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
# Null coalescing operator
`%||%` <- function(a, b) if (is.null(a)) b else a

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R/utils.R defines a local %||% operator while the package already imports purrr, which exports an equivalent %||%. Keeping a separate implementation with the same name increases ambiguity (masking) without adding functionality. Consider removing this definition and relying on purrr::%||%`` (or, if you want to avoid purrr here, stop importing purrr wholesale and importFrom() only what’s needed).

Suggested change
# Null coalescing operator
`%||%` <- function(a, b) if (is.null(a)) b else a

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +203
args_parts <- c(glue('"id": "{filter_id}"'))

if (!is.null(new_name)) {
args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_filter() inserts filter_id into the Sync API command JSON without escape_json(). For consistent/robust JSON generation (and to prevent malformed payloads if IDs ever contain escapable characters), escape filter_id before interpolating it.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +202
args_parts <- c(glue('"id": "{label_id}"'))

if (!is.null(new_name)) {
args_parts <- c(args_parts, glue('"name": "{escape_json(new_name)}"'))
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_label() interpolates label_id into the Sync API command JSON without escape_json(). Escape label_id (and other string fields) to avoid malformed JSON and to match the escaping approach used elsewhere (e.g., update_task).

Copilot uses AI. Check for mistakes.
@VincentGuyader
Copy link
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copilot AI added 4 commits February 9, 2026 17:58
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #20, to work on those changes. Once the pull request is ready, I'll request review from you.

@VincentGuyader
Copy link
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link

Copilot AI commented Feb 9, 2026

@VincentGuyader I've opened a new pull request, #21, to work on those changes. Once the pull request is ready, I'll request review from you.

@VincentGuyader
Copy link
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link

Copilot AI commented Feb 10, 2026

@VincentGuyader I've opened a new pull request, #23, to work on those changes. Once the pull request is ready, I'll request review from you.

* Add escape_json() to all Sync API commands with ID parameters
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 116 out of 117 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

R/tasks.R Outdated
Comment on lines +498 to +502
if (!is.null(project_id)) {
args_parts <- c(args_parts, glue('"project_id": "{project_id}"'))
}
if (!is.null(section_id)) {
args_parts <- c(args_parts, glue('"section_id": "{section_id}"'))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move_task() can be called with project_id, section_id, and parent_id all left as NULL, but it will still send an item_move command (with only an id arg). That request is likely invalid and will fail at runtime. Add input validation to require at least one destination argument be provided.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
result <- data.frame(
id = character(),
name = character(),
stringsAsFactors = FALSE
)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “empty workspaces returns empty dataframe” test builds an empty result without the is_default column, but get_all_workspaces() returns id, name, and is_default even when empty. Update the test to include/assert is_default so it matches the function behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
result <- data.frame(
id = character(),
object_type = character(),
object_id = character(),
event_type = character(),
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “empty activity logs returns empty dataframe” test constructs an empty result without initiator_id/parent_project_id/parent_item_id, but get_activity_logs() includes these columns in its empty return. Update the expected empty structure so the test matches the function’s output.

Copilot uses AI. Check for mistakes.
R/templates.R Outdated
Comment on lines +34 to +39
request(as.character(glue("{TODOIST_REST_URL}templates/import_into_project?project_id={project_id}"))) %>%
req_headers(
Authorization = glue::glue("Bearer {token}")
) %>%
req_body_multipart(file = curl::form_file(file_path)) %>%
req_error(is_error = function(resp) resp_status(resp) >= 400) %>%
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import_template() manually concatenates the query string into the URL. Using req_url_query(project_id = project_id) (and a base URL without the query) would ensure proper URL-encoding and is more robust than string interpolation, especially if IDs are not strictly numeric.

Copilot uses AI. Check for mistakes.
@VincentGuyader
Copy link
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link

Copilot AI commented Feb 12, 2026

@VincentGuyader I've opened a new pull request, #24, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 7 commits February 13, 2026 16:01
…oding (#24)

* Address PR review feedback: validation, test accuracy, and URL handling
- Add @return tags to add_section() and get_section_id()
- Remove unused httr dependency (replaced by httr2)
- Remove unused lubridate from Suggests
- Update LICENSE file format (2019-2026, ThinkR)
- Remove dead mock_response() code and rename test file
- Add REVIEW.md with pre-submission package audit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Escape IDs in move_task() with escape_json() to prevent JSON injection
- Add token validation in call_api() and call_api_rest()
- Replace print() with message() in users.R for CRAN compliance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document all 64 new exported functions across 9 new modules:
- Labels, Comments, Filters, Reminders, Workspaces
- Activity logs, Backups, Templates, Uploads

Plus enhancements to Projects, Sections, Tasks, and Collaboration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace lubridate::today() and days() with base R Sys.Date() and
arithmetic operations for consistency with removed Suggests dependency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@VincentGuyader VincentGuyader merged commit bd7dba7 into master Feb 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants