Skip to content

test: add unit test suite reaching 80% coverage on all metrics (cascades on #30)#31

Merged
vitorfdl merged 18 commits intofeat/cli-refactoring-depsfrom
feat/cli-refactoring-tests
Apr 27, 2026
Merged

test: add unit test suite reaching 80% coverage on all metrics (cascades on #30)#31
vitorfdl merged 18 commits intofeat/cli-refactoring-depsfrom
feat/cli-refactoring-tests

Conversation

@mateuscardosodeveloper
Copy link
Copy Markdown
Collaborator

@mateuscardosodeveloper mateuscardosodeveloper commented Apr 22, 2026

Summary

Cascading PR on top of #30. Adds the full unit-test suite and wires CI to enforce coverage, fulfilling the SPEC requirement of ≥ 80% across all four metrics.

Merges only after #30 lands. Contains no production-code changes — only tests, test utilities, and one CI workflow bump.

Coverage result

Metric Coverage
Lines 86.63%
Statements 86.49%
Functions 82.50%
Branches 80.01%

npx vitest run --coverage exits 0 on this branch.

What this PR contains

Commit 1 — test: add 71 unit test files to reach 80% line coverage

Adds the initial test suite from scratch: 71 new files, 323 tests. Lifts line coverage from baseline to ~80%.

Commit 2 — test: raise branch coverage above 80% and wire CI to test:coverage

Expands 26 existing test files to close branch gaps, bringing all four metrics above 80%. Also updates .github/workflows/code-quality.yml to run npm run test:coverage (not npm test), so the threshold is enforced on every push.

Patterns used across the suite

  • Proxy-based Account/Resources mock via makeAccount (added in refactor: modernize toolchain, migrate to ESM, and remove axios/lodash #30).
  • makeEnvironmentConfig factory for config stubs.
  • resetInjectedPrompts() between tests to prevent prompts.inject() leakage across cases.
  • installFetchMock + helpers (makeFetchResponse, makeFetchArrayBufferResponse, makeFetchStreamResponse) for the fetch-migrated code paths.
  • Dynamic await import("./foo.js") so vi.mock() factories hoist cleanly when the module depends on mocked globals or SDK constructors. Note: there's a potential follow-up cleanup PR to convert mock-function-access patterns into top-level mock constants — left out of this PR to keep scope tight.
  • Fake timers (vi.useFakeTimers) for async.queue resources with setTimeout-based pacing.

Known gaps (documented, not blocking the 80% thresholds)

  • src/commands/profile/backup/restore.ts — 9% branches. Testing would need: a real ZIP fixture streamed through fetch, coordinating pipeline() + unzipper.Extract().promise() on disk, and ~13 cascaded SDK mocks across every backup resource type. Fragile and high LOC for low marginal value. Deferred.
  • src/commands/start-config.ts — 17% branches. Deeply interactive (12+ chained prompts() calls, real readdirSync filesystem walks, multi-step login loopback). Covering well would require long, brittle prompts.inject([...]) sequences plus tmpdir setup. Deferred.
  • One latent async.queue.drain() hang when the queue never receives items (source behavior, not introduced by this PR). All test fixtures force at least one item into the queue to avoid triggering it.

Test plan

Expands the test suite from 35 to 358 passing tests, lifting line
coverage from ~45% to 80.16% (above the SPEC threshold of 80%).

Coverage per layer:
- lib/: 89.8%
- prompt/: 95.5%
- commands/: 74.3%
- profile/backup/resources/: 84.5%

Patterns standardized across the suite:
- Proxy-based Account/Resources mock via makeAccount (shared helper)
- makeEnvironmentConfig factory for config stubs
- resetInjectedPrompts between tests to avoid prompts.inject() leakage
- installFetchMock + makeFetchResponse/ArrayBuffer/Stream helpers
- Dynamic `await import("./foo.js")` so vi.mock() factories hoist cleanly
- Fake timers (vi.useFakeTimers) for async.queue resources with
  setTimeout-based pacing

Notes on known gaps:
- `src/commands/profile/backup/restore.ts` (9% covered) exercises axios
  streams + unzipper in ways that are not easily mocked without a real
  zip fixture; left as-is.
- `src/commands/start-config.ts` (21%) has deeply interactive branches
  that would need heavy prompt scripting to cover fully.
- One latent `async.queue.drain()` hang when the queue never receives
  items is documented in fixtures (all test fixtures force at least one
  item to avoid triggering it).

CI impact: `npm run linter` → 0/0 on 170 files; `npm test` → 358/358.
Expands existing test files across the suite to lift branch coverage
from 73.9% to 80.01%, closing the last remaining SPEC threshold.

Coverage per metric (final):
- Lines      86.63%
- Statements 86.49%
- Functions  82.50%
- Branches   80.01%

All four thresholds ≥ 80%, matching the SPEC acceptance criteria.

Files expanded (targeting highest-ROI branch gaps):
- backup/resources/*: add granularItem selection flows (10 files)
- profile/export/services: collect-ids, widgets-export,
  dashboards-export, devices-export, analysis-export, actions-export
- profile/export/export.ts: per-entity isolation tests that exercise
  the "collect dependencies first" branches that single-entity runs
  trigger (devices alone, dashboards alone, access alone, etc.)
- profile/backup/create.ts: non-ok POST, custom API base,
  null-backup poll continue
- profile/backup/lib.ts: 2FA-cancel and pin-cancel paths
- commands: login (URL helper branches + deploy URL derivation),
  deploy (analysis.info reject, upload failure, default paths),
  duplicate-analysis (pick cancellation, fetch non-ok),
  change-network (both prompts empty, partial override),
  copy-tab (prompt for dashboard id, tab pick cancel, no arrangement)
- lib: token (empty folder branch), config-file (create-on-missing,
  unparseable file, writeConfigFileEnv, writeToConfigFile,
  setDefault, resolveCLIPath), notify-update (fetch helper +
  getLatestVersion)
- devices: data-post (prompt path, fallback to Device lookup,
  silent-fail branch), analysis-console (scriptObj unresolved,
  onmessage, onopen, onerror)

Restore.ts (9% coverage) and start-config.ts (21%) intentionally
left as-is:
- restore.ts: streams + unzipper + disk I/O + ~13 cascaded SDK
  calls across every backup resource would require a real zip
  fixture and is fragile to mock.
- start-config.ts: deeply interactive prompt chains with tmpdir
  filesystem walks. Extensive prompt scripting would be brittle.

CI: switch `code-quality.yml` from `npm test` to `npm run
test:coverage` so the 80% threshold is actually enforced, and bump
`actions/checkout` and `actions/setup-node` to v6.
Adds --all to deploy every analysis in tagoconfig.json without any
prompt, and -t/--token to supply a profile token at invocation time
(bypassing the lock file, which doesn't exist in CI runners).

Also rejects the legacy "deploy all" positional with a helpful
pointer to --all. The "all" positional has never worked as the help
text suggested — it opened an interactive prompt — so dropping it
is a bug fix, not a breaking change.

Help examples updated: remove the misleading "deploy all" line and
add pipeline-friendly usages covering --all, --all + stage-env,
and the full CI combination (--all -e prod -t $TAGOIO_TOKEN --silent).

Surgical scope: no changes to init, login, or any shared
infrastructure. Only deploy.ts and analysis/index.ts are touched.
Documents the deploy flow for CI runners using --all + -e + -t +
--silent, including the install step for @tago-io/cli and
@tago-io/builder (deploy shells out to analysis-builder, so the
builder needs to be on PATH).

Notes that -t accepts either a profile token or an external-analysis
token with the proper Access Management permissions, and that a
pre-configured tagoconfig.json in the repository is required.
Adds coverage for the four new behaviors:
- legacy positional "all" is rejected with a pointer to --all
- --all deploys every analysis with no prompt
- -t/--token overrides the lock-file token for the run
- --all + -t end-to-end works with no lock file (CI flow)

Also updates the existing "env missing" test to reflect the new
error message ("No profile token found" instead of "Environment not
found") and introduces a defaultOptions() helper to DRY up the test
setup now that IDeployOptions has 5 required fields.
mateuscardosodeveloper and others added 11 commits April 22, 2026 17:45
…gister

`tagoio run <name>` was broken on every Node run: the command called
`node -r <swc-register-path>` via `resolveCLIPath`, but that package
was removed during Phase 1 when SWC was ripped out. Every invocation
failed with `Cannot find module '.../node_modules/@swc-node/register/index'`.

Switches the Node path in `_buildCMD` to `node --experimental-strip-types
--watch`. Node 24+ strips TypeScript type annotations natively, so no
external loader is required. The flag is stable on Node 24 and does
not emit a runtime warning (unlike `--experimental-transform-types`,
which does). Limitation: strip-types does not handle `enum` or
`namespace` — not used in customer analyses today.

Also:
- Drops the now-unused `resolveCLIPath` import from run-analysis.ts.
- Updates the three `_buildCMD` tests that asserted the old SWC path.
- README: removes the `.swcrc` / `.swcrc.example` note from the
  Analysis Runner section (the debugger no longer needs source-map
  config — Node's strip-types preserves line numbers).
- README (CI/CD): documents the Access Management rule required when
  using an external-analysis token in `-t, --token` (Access Analysis
  + Edit Analysis + Upload Analysis Script), so least-privilege
  pipelines don't hit Authorization Denied at deploy time.
- `analysis/index.ts`: spells out `--env` instead of `-e` in the help
  examples for consistency with the option name shown above the list.
`tagoio run <name>` was broken for legacy analysis projects that use
`"module": "CommonJS"` + `"moduleResolution": "Node"` in their tsconfig
and import subpaths without a `.js` extension (e.g.
`import { Data } from "@tago-io/sdk/lib/types"`). Commit 60c5792 had
swapped the runner to `node --experimental-transform-types --watch`, but
Node's built-in TS execution routes those files through the ESM loader,
which rejects extensionless specifiers with `ERR_MODULE_NOT_FOUND`.
Every legacy analysis failed at import time.

Switches `_buildCMD` to invoke tsx's CLI via the cached binary shipped
inside the CLI's own node_modules:
`node <cli-path>/node_modules/tsx/dist/cli.mjs watch <script>`. tsx
pairs esbuild (TS transpile) with oxc-resolver and CJS-aware module
resolution, so the same classic imports resolve cleanly. Watch mode is
provided by `tsx watch` instead of `node --watch`, and `--inspect` /
`--clear` continue to work.

Why tsx over reviving `@swc-node/register`:
- Install footprint: ~12 MB per platform (esbuild single native binary)
  vs ~25-36 MB for @swc/core + oxc-resolver
- One native binary to ship, not two
- Watch + REPL ship built-in
- Same resolver internally (oxc-resolver), so no correctness difference
- 10x the weekly npm downloads and stronger maintenance signal

Also:
- Adds `tsx@^4.21.0` to dependencies.
- Reintroduces the `resolveCLIPath` import removed in 60c5792.
- Updates the three `_buildCMD` tests that asserted the Node native
  flags to assert the tsx CLI path and `watch` subcommand instead.

The `--tsnd` and `--deno` flags are unchanged.
Device restore was only recreating the device record itself. Two
pieces of state that users expect to survive a backup/restore cycle
were silently dropped:

- Configuration parameters (`device.params`) — these carry per-device
  runtime settings and are required by most integrations.
- Tokens tied to a physical device by `serie_number` — without these,
  the device cannot communicate after restore.

Adds three helpers in `devices.ts`:

- `stripDeviceFields` centralizes the list of fields the TagoIO API
  rejects on create/edit (ids, timestamps, profile, and the two
  restored-separately fields `params` and `tokens`). Both
  `processCreateTask` and `processEditTask` now delegate to it,
  removing the previously duplicated destructuring.
- `restoreDeviceParams` replays the backup's params via the dedicated
  `paramSet` endpoint.
- `restoreDeviceTokens` recreates only tokens with a `serie_number`
  (the identifying attribute for physical devices); ephemeral tokens
  without one are intentionally skipped. On the edit path it fetches
  the current token list first and skips serials already present, so
  re-runs are idempotent. Token values cannot be preserved because
  the backup stores them masked — callers must update integrations
  that relied on the old value.

Tests cover: param restoration on create/edit, serial-token
recreation, skipping tokens without a serial, idempotency when a
serial already exists on the target device, and graceful handling of
a failed `tokenCreate` (logged, restore continues).
feat(cli): CI/CD flags, fix legacy TS runner, restore device params and tokens (cascades on #31)
@vitorfdl vitorfdl merged commit 57d756f into feat/cli-refactoring-deps Apr 27, 2026
1 check passed
@vitorfdl vitorfdl deleted the feat/cli-refactoring-tests branch April 27, 2026 13:56
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.

2 participants