fix(orbit): preserve @ in JSON string literals for orbit remote query

What does this MR do and why?

Fixes a confusing failure mode of glab orbit remote query reported as the "Invalid character '@'" bug: valid JSON payloads that contain @ inside string literals (e.g. email addresses like user@example.com, Ruby @instance_var references, @version annotations) appear to be rejected by the CLI.

gitlab#600060

Symptom (user-facing report)

glab orbit remote query /tmp/q.json returns Invalid character '@' even though jq . /tmp/q.json validates successfully.

Root cause

The glab orbit remote query command itself never preprocesses @ — it reads the body, hands the inner query field through as json.RawMessage, and lets the API client send the bytes verbatim. So @ inside JSON string literals is in fact already preserved end-to-end.

What was actually surfacing the report was the error message the CLI emits when JSON parsing fails on a stray @ outside a string literal — for example, an unrendered @variable placeholder left over from a templating tool, or a byte shifted by a UTF-8 BOM that some editors prepend on save. The error came straight from encoding/json as invalid character '@' looking for beginning of value and was repeatedly misread as the CLI mangling @ inside string values. jq accepts BOMs and may silently mask the same defect, which is why the same file "validates with jq" but fails through glab.

Fix

The body still receives zero preprocessing; the change makes that contract explicit and improves the failure mode:

  • buildRequest documents in its godoc that @ inside JSON string literals is forwarded verbatim — no curl-style @filename expansion, no field-magic.
  • readBody strips a leading UTF-8 BOM so files saved as "UTF-8 with BOM" parse the same way jq accepts them.
  • When JSON parsing fails on a stray @ at the top level, the error is wrapped with a clear hint that @ inside string values is fine and that jq . <file> will pinpoint the offending byte. The hint is baked into the error string itself because Fang's default error handler renders err.Error() and ignores ExitError.Details.

New tests

  • TestBuildRequest_PreservesAtInStringLiterals — locks in the verbatim contract for @ inside string literals (email addresses, Ruby @instance_var references, @version annotations, @-prefixed values, multiple @s).
  • TestBuildRequest_InvalidJSON_AtCharacterOutsideString — verifies the hinted error message points at the stray @, mentions that string-literal @s are fine, and points at jq.
  • TestBuildRequest_InvalidJSON_NonAtCharacter — verifies the generic message is still used for non-@ syntax errors.
  • TestReadBody_StripsUTF8BOM — verifies BOM stripping.
  • TestQuery_FromFile_WithAtInStrings — end-to-end regression for the file-input path with @s in string values.
  • TestQuery_Stdin_WithAtInStrings — end-to-end regression for the stdin path.

How to set up and validate locally

Click to expand
# 1. `@` inside string literals is preserved end-to-end (no rejection).
cat > /tmp/q.json << 'JSON'
{"query": {"query_type": "traversal", "node": {"id": "p", "entity": "Project"}, "filter": {"email": "user@example.com", "note": "see @ahegyi"}, "limit": 1}}
JSON
glab orbit remote query /tmp/q.json
# → request is forwarded to the API (you'll see the API's response or a
#   server-side schema error, but the CLI no longer rejects the body
#   on the `@` characters).

# 2. Stray `@` outside a string literal now produces a helpful error
#    instead of the bare stdlib message.
cat > /tmp/bad.json << 'JSON'
{"query": @variable}
JSON
glab orbit remote query /tmp/bad.json
# →  Query body is not valid JSON: stray '@' outside a string literal
#    at byte 11. `@` is allowed inside JSON string values
#    (e.g. "user@example.com"); if your file looks correct, validate
#    it with `jq . <file>` to find the real offset: invalid character
#    '@' looking for beginning of value.

# 3. BOM-prefixed files now parse the same way `jq` accepts them.
printf '\xEF\xBB\xBF{"query":{"query_type":"traversal","node":{"id":"p","entity":"Project"},"limit":1}}' > /tmp/bom.json
glab orbit remote query /tmp/bom.json
# → request is forwarded (no "invalid character 'ï'").

# 4. Run the new tests
cd ~/workspace/cli
go test ./internal/commands/orbit/remote/query/...

Compatibility

No CLI surface changes: same command, same flags, same exit codes. Behaviour change is strictly additive (better error message) plus the BOM-strip, which only affects inputs that were previously rejected outright.

Edited by Dmitry Gruzd

Merge request reports

Loading