Release 2.2.0 — unified onboard strategy + safety hardening
Folds `josh-sync adopt` into `josh-sync onboard <target>` as the adopt strategy alongside the original reset flow. Strategy resolves by precedence: --mode flag > targets[].history_lock config (preserve|rewrite) > auto-detect via `git ls-remote --heads`. `josh-sync adopt` is kept as a thin alias. Adds the new `targets[].history_lock` config field (validated at parse time) and folds `<target>/adopt.json` state into `<target>/onboard.json` with a `.strategy` field; legacy state files are read with a backward-compat fallback. Safety hardening (from a multi-angle review of the unification): - auto-detect distinguishes auth/network failure from empty repo - resume validates --mode against the strategy saved in state - `josh-sync adopt` rejects a conflicting --mode in the forwarded args - missing import-PR lookup dies in both strategies (was WARN+continue for reset, which could create duplicate import PRs on resume) - --restart durably removes the legacy adopt.json from the state branch - adopt_branch is now a subshell function (EXIT trap can't clobber callers) - strategy value validated after resolve/load (reset|adopt) - --mode with missing/empty value dies with a usage hint - migrate-pr against an adopt-strategy target dies with a specific hint - reset importing asserts archived_url is present (no "null" → git clone) End-to-end + CLI bats coverage added (tests/unit/adopt_e2e.bats, tests/unit/cli.bats). 72 tests, shellcheck clean. Makefile dist bundle header now correctly interpolates VERSION and line count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
123
tests/unit/cli.bats
Normal file
123
tests/unit/cli.bats
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
bats_require_minimum_version 1.5.0
|
||||
|
||||
# tests/unit/cli.bats — bin/josh-sync CLI surface tests
|
||||
#
|
||||
# Invokes the CLI as a subprocess (not by sourcing libs) so flag parsing,
|
||||
# command dispatch, help/usage text, and the `adopt → onboard --mode=adopt`
|
||||
# alias regress cleanly. These tests deliberately stay at the parse/dispatch
|
||||
# layer — anything that would reach out to git remotes is set up to fail at
|
||||
# target-lookup time, before any side effects.
|
||||
#
|
||||
# Uses `run --separate-stderr` so we can assert on stderr separately (most of
|
||||
# josh-sync's output is logged via core.sh's `log()` which writes to stderr).
|
||||
|
||||
setup() {
|
||||
JOSH_SYNC_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
JOSH_BIN="${JOSH_SYNC_ROOT}/bin/josh-sync"
|
||||
FIXTURES="${JOSH_SYNC_ROOT}/tests/fixtures"
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
cd "$BATS_TEST_DIRNAME" 2>/dev/null || true
|
||||
rm -rf "$TEST_ROOT"
|
||||
}
|
||||
|
||||
# Drop the minimal fixture config into a clean cwd. Most cmd_* paths read
|
||||
# .josh-sync.yml from the working directory.
|
||||
seed_minimal_config() {
|
||||
cp "${FIXTURES}/minimal.yml" "${TEST_ROOT}/.josh-sync.yml"
|
||||
cd "$TEST_ROOT"
|
||||
}
|
||||
|
||||
# ─── Global flags ───────────────────────────────────────────────────
|
||||
|
||||
@test "--version exits 0 and prints the version" {
|
||||
run "$JOSH_BIN" --version
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"josh-sync"* ]]
|
||||
}
|
||||
|
||||
@test "--help exits 0 and advertises onboard, the adopt alias, and --mode" {
|
||||
run "$JOSH_BIN" --help
|
||||
[ "$status" -eq 0 ]
|
||||
# Help text is printed to stderr via `cat >&2`, so combine with --separate.
|
||||
run --separate-stderr "$JOSH_BIN" --help
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$stderr" == *"onboard <target>"* ]]
|
||||
[[ "$stderr" == *"adopt <target>"* ]]
|
||||
[[ "$stderr" == *"Alias for 'onboard --mode=adopt'"* ]]
|
||||
[[ "$stderr" == *"--mode={reset,adopt}"* ]]
|
||||
}
|
||||
|
||||
# ─── onboard flag parsing & dispatch ────────────────────────────────
|
||||
|
||||
@test "onboard with no target exits non-zero and prints usage including --mode" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" onboard
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"Usage: josh-sync onboard"* ]]
|
||||
[[ "$stderr" == *"--mode=reset|adopt"* ]]
|
||||
}
|
||||
|
||||
@test "onboard --mode=invalid is rejected with a clear error" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" onboard --mode=invalid example
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"Invalid --mode"* ]]
|
||||
}
|
||||
|
||||
@test "onboard --mode=adopt against an unknown target reports target-not-found" {
|
||||
seed_minimal_config
|
||||
# Proves --mode parses correctly: parse_config succeeds, then cmd_onboard
|
||||
# fails at target lookup, before any git/network activity.
|
||||
run --separate-stderr "$JOSH_BIN" onboard --mode=adopt nonexistent
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"not found"* ]]
|
||||
}
|
||||
|
||||
# ─── adopt alias ────────────────────────────────────────────────────
|
||||
|
||||
@test "adopt alias forwards to onboard --mode=adopt (visible via alias log line + target lookup)" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" adopt nonexistent
|
||||
[ "$status" -ne 0 ]
|
||||
# Alias notice from cmd_adopt:
|
||||
[[ "$stderr" == *"alias for: josh-sync onboard --mode=adopt"* ]]
|
||||
# And the forwarded call lands in cmd_onboard's target lookup:
|
||||
[[ "$stderr" == *"not found"* ]]
|
||||
}
|
||||
|
||||
@test "onboard usage listing shows --restart and --mode flags together" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" onboard
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"--restart"* ]]
|
||||
[[ "$stderr" == *"--mode"* ]]
|
||||
}
|
||||
|
||||
# ─── --mode value-parsing guards ────────────────────────────────────
|
||||
|
||||
@test "onboard --mode (no value) fails with a helpful message instead of crashing on unbound \$2" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" onboard example --mode
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"--mode requires a value"* ]]
|
||||
}
|
||||
|
||||
@test "onboard --mode= (empty after equals) is rejected explicitly, not silently ignored" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" onboard example --mode=
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"Empty --mode="* ]]
|
||||
}
|
||||
|
||||
@test "adopt alias dies when a conflicting --mode=reset is also passed (instead of silently honouring reset)" {
|
||||
seed_minimal_config
|
||||
run --separate-stderr "$JOSH_BIN" adopt example --mode=reset
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$stderr" == *"alias for --mode=adopt"* ]]
|
||||
[[ "$stderr" == *"conflicting"* ]]
|
||||
}
|
||||
Reference in New Issue
Block a user