App-karr
view release on metacpan or search on metacpan
t/46-global-dir-option.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use lib 't/lib';
use TestGit qw( require_git_c );
require_git_c();
use TestKarr qw( run_karr );
use File::Temp qw( tempdir );
# Regression tests for karr board ticket #14.
#
# BUG: the documented global --dir option is silently ignored by dispatched
# subcommands. `karr --dir /path/to/B create "X"` (root form) writes into
# the CWD's board instead of B's, with exit 0 and no diagnostic --
# App::karr.pm documents "--dir overrides" board discovery and declares
# `option dir => (...)` on the ROOT class, but MooX::Cmd dispatches to
# App::karr::Cmd::* objects that never get that attribute, so
# BoardDiscovery::_build_git_root's `$self->can('has_dir') && $self->has_dir`
# check is always false on the dispatched Cmd and discovery falls back to
# cwd. Cmd::Init/Backup/Restore/Destroy additionally hardcode
# `Git->new(dir => '.')` outright, bypassing BoardDiscovery entirely. This
# exact shape (root-form --dir silently targeting cwd) caused real
# accidental writes to the maintainer's dogfood board during ticket #13,
# remediated from dangling commits.
#
# DESIGN (recorded on ticket #14, `karr show 14`): both call shapes must work
# or fail LOUDLY -- never silently fall back to cwd:
# - `karr CMD --dir X` (Cmd-level option)
# - `karr --dir X CMD` (root form; MooX::Cmd passes command_chain to
# execute(), the Cmd adopts the root's dir)
# Discovery starts at the given path and still walks up looking for a Git
# repo (parity note below). Invalid --dir (nonexistent path, or an
# existing directory that isn't a Git repo) must produce a clean,
# non-zero-exit error and must NOT operate on the cwd board. init/backup
# (and by extension restore/destroy) must honour --dir too.
#
# PARITY CHECK (../kanban-md/cmd/root.go, requested on this ticket before
# writing coverage): kanban-md's --dir is a cobra PersistentFlag bound to
# a single package-level var, so both "before" and "after" the subcommand
# name are equivalent to cobra's parser -- there is no root-form/cmd-form
# distinction there, which is *why* kanban-md never had this bug shape.
# Its resolveDir() returns flagDir verbatim with NO walk-up when set
# (internal/config.FindDir's walk-up only applies to the unset/cwd case).
# karr's already-agreed design deliberately diverges here: --dir is a
# Git-repo discovery seed, not a literal kanban-directory path, so karr's
# walk-up applies even when --dir is given (see DESIGN above). This is a
# recorded, intentional divergence, not an open question.
#
# None of this is implemented yet (no lib/ changes accompany this test).
# Every subtest below was hand-probed against the current tree (VERSION
# 0.304, pre-#14-fix) before writing assertions; the exact current behaviour
# is recorded per subtest. All of them assert the desired post-fix contract,
# so all are expected RED until #14 lands:
#
# - Cmd-form (`CMD --dir X`) on list/show/backup/init: today MooX::Options
# doesn't know a `dir` option on the dispatched Cmd class at all, so it
# dies "Unknown option: dir" (exit 1) before touching any board -- an
# accidental non-zero exit, but for the wrong reason and with the wrong
# message, not a validated/clean --dir error.
# - Root-form (`--dir X CMD`) on list/show/backup: today silently succeeds
# (exit 0) against the CWD's board, never touching X at all -- the exact
# incident shape.
# - Root-form on init: today dies "Board already exists in refs/karr/"
# (exit 255) because it hardcodes dir => '.' and finds the CWD's
# pre-existing board -- again non-zero, but for the wrong reason, and it
# never touches the target repo.
# - Invalid --dir in root form: today silently succeeds (exit 0) against
# the CWD board -- the most dangerous shape, explicitly called out in the
# ticket as "never silently fall back".
# In-process runner (t/lib/TestKarr.pm): same ($cwd, @argv) signature and
# { exit, stdout, stderr } return as the open3 helper this file used to carry,
# dispatched through the shared App::karr::Dispatch path. KARR_TEST_SUBPROC=1
# restores the old open3 path.
sub _run_karr { return run_karr(@_) }
sub _git_ok {
my (@cmd) = @_;
my $rc = system(@cmd);
is( $rc, 0, "@cmd" );
}
# Raw dump of every ref under refs/karr/* for a repo, read directly via git
# (never through karr itself), so "board A is untouched" assertions don't
# depend on the very discovery/read path this ticket is about fixing.
sub _refs_dump {
my ($repo) = @_;
my @cmd = ( 'git', '-C', $repo, 'for-each-ref',
'--format=%(refname) %(objectname)', 'refs/karr' );
open( my $fh, '-|', @cmd ) or die "can't run @cmd: $!";
local $/;
my $out = <$fh>;
close $fh;
return defined $out ? $out : '';
}
# Fresh isolated git repo + karr board, deliberately WITHOUT a remote (so
# sync_before/sync_after retry loops can't interfere with or slow down these
# assertions), seeded with one task whose title embeds $label so board A's
# and board B's output are trivially distinguishable from each other no
# matter which board a given command actually landed on.
sub _setup_board {
my ($label) = @_;
my $repo = tempdir( CLEANUP => 1 );
_git_ok( 'git', 'init', '-q', $repo );
_git_ok( 'git', '-C', $repo, 'config', 'user.email', 'test@example.com' );
_git_ok( 'git', '-C', $repo, 'config', 'user.name', 'Test User' );
my @remotes = `git -C '$repo' remote`;
is( scalar(@remotes), 0, "board $label repo has no remote configured" );
my $init = _run_karr( $repo, 'init', '--name', "Board $label" );
is( $init->{exit}, 0, "karr init succeeds for board $label" ) or diag $init->{stderr};
my $create = _run_karr( $repo, 'create', "$label-Only Seed Task" );
is( $create->{exit}, 0, "seed task created for board $label" ) or diag $create->{stderr};
return $repo;
}
t/46-global-dir-option.t view on Meta::CPAN
subtest '--dir B show 1 (root-form) shows B\'s task 1, not A\'s (RED, ticket #14)' => sub {
my $rv = _run_karr( $A, '--dir', $B, 'show', 1 );
# Probed today: exit 0, silently shows A's own task #1
# ("Task #1: A-Only Seed Task").
is( $rv->{exit}, 0, '--dir B show 1 exits 0' ) or diag $rv->{stderr};
like( $rv->{stdout}, qr/^Task #1: B-Only Seed Task$/m, 'shows B\'s task 1 by its distinguishing title' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'does NOT show A\'s task' );
};
# --------------------------------------------------------------- (5) Init on
# a fresh, boardless repo via --dir, in both call shapes. A already has a
# board (from setup), so a correct fix must neither touch A nor complain
# about A's pre-existing board.
subtest 'init --dir C (cmd-form) creates the board in C, not A (RED, ticket #14)' => sub {
my $C = tempdir( CLEANUP => 1 );
_git_ok( 'git', 'init', '-q', $C );
_git_ok( 'git', '-C', $C, 'config', 'user.email', 'test@example.com' );
_git_ok( 'git', '-C', $C, 'config', 'user.name', 'Test User' );
my $before_A = _refs_dump($A);
my $rv = _run_karr( $A, 'init', '--dir', $C, '--name', 'Board C' );
# Probed today: dies "Unknown option: dir" (exit 1) -- Cmd::Init has no
# `dir` option and never touches C at all.
is( $rv->{exit}, 0, 'init --dir C exits 0' ) or diag $rv->{stderr};
is( _refs_dump($A), $before_A, "board A's refs are unchanged by init --dir C" );
my $c_refs = _refs_dump($C);
like( $c_refs, qr{refs/karr/config}, 'refs/karr/config now exists in C' );
};
subtest '--dir C2 init (root-form) creates the board in C2, not A (RED, ticket #14)' => sub {
my $C2 = tempdir( CLEANUP => 1 );
_git_ok( 'git', 'init', '-q', $C2 );
_git_ok( 'git', '-C', $C2, 'config', 'user.email', 'test@example.com' );
_git_ok( 'git', '-C', $C2, 'config', 'user.name', 'Test User' );
my $before_A = _refs_dump($A);
my $rv = _run_karr( $A, '--dir', $C2, 'init', '--name', 'Board C2' );
# Probed today: dies "Board already exists in refs/karr/\n" (exit 255)
# -- Cmd::Init hardcodes Git->new(dir => '.'), which resolves to A (the
# CWD), which already has a board from setup. C2 is never touched.
is( $rv->{exit}, 0, '--dir C2 init exits 0' ) or diag $rv->{stderr};
is( _refs_dump($A), $before_A, "board A's refs are unchanged by --dir C2 init" );
my $c2_refs = _refs_dump($C2);
like( $c2_refs, qr{refs/karr/config}, 'refs/karr/config now exists in C2' );
};
# ------------------------------------------------------------- (6) Backup on
# B via --dir, in both call shapes.
subtest 'backup --dir B (cmd-form) snapshots B, not A (RED, ticket #14)' => sub {
my $rv = _run_karr( $A, 'backup', '--dir', $B );
# Probed today: dies "Unknown option: dir" (exit 1) -- Cmd::Backup has
# no `dir` option.
is( $rv->{exit}, 0, 'backup --dir B exits 0' ) or diag $rv->{stderr};
like( $rv->{stdout}, qr/name: Board B/, 'snapshot contains board B\'s name' );
like( $rv->{stdout}, qr/B-Only Seed Task/, 'snapshot contains B\'s seed task' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'snapshot does NOT contain A\'s seed task' );
};
subtest '--dir B backup (root-form) snapshots B, not A (RED, ticket #14)' => sub {
my $rv = _run_karr( $A, '--dir', $B, 'backup' );
# Probed today: exit 0, silently dumps A's own snapshot (name: Board A,
# A-Only Seed Task, plus the "Neu In B" task accidentally created there
# by an earlier probe) -- Cmd::Backup hardcodes dir => '.'.
is( $rv->{exit}, 0, '--dir B backup exits 0' ) or diag $rv->{stderr};
like( $rv->{stdout}, qr/name: Board B/, 'snapshot contains board B\'s name' );
like( $rv->{stdout}, qr/B-Only Seed Task/, 'snapshot contains B\'s seed task' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'snapshot does NOT contain A\'s seed task' );
};
# ------------------------------------------------------------ (7) Invalid
# --dir must fail loudly and cleanly, never silently fall back to A. Uses
# fresh System::TMPDIR-rooted paths (never nested under A/B/the repo) so the
# walk-up-inclusive design (see PARITY CHECK above) can't accidentally climb
# into an unrelated real repository above them.
subtest 'list --dir /nonexistent (cmd-form): clean error, no fallback to A (RED, ticket #14)' => sub {
my $bogus_parent = tempdir( CLEANUP => 1 );
my $bogus = "$bogus_parent/does-not-exist";
my $rv = _run_karr( $A, 'list', '--dir', $bogus, '--compact' );
# Probed today: dies "Unknown option: dir" (exit 1) -- non-zero, but for
# the wrong reason (option parsing, not path validation).
isnt( $rv->{exit}, 0, 'list --dir /nonexistent exits non-zero' );
like( $rv->{stderr}, qr/not a git repository/i, 'stderr gives a clean "not a git repository" diagnostic' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'stdout shows nothing from board A' );
};
subtest '--dir /nonexistent list (root-form): clean error, no silent fallback to A (RED, ticket #14)' => sub {
my $bogus_parent = tempdir( CLEANUP => 1 );
my $bogus = "$bogus_parent/does-not-exist";
my $rv = _run_karr( $A, '--dir', $bogus, 'list', '--compact' );
# Probed today: exit 0, silently prints A's own list -- the dangerous
# silent-fallback shape the ticket explicitly forbids.
isnt( $rv->{exit}, 0, '--dir /nonexistent list exits non-zero' );
like( $rv->{stderr}, qr/not a git repository/i, 'stderr gives a clean "not a git repository" diagnostic' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'stdout shows nothing from board A' );
};
subtest '--dir <existing, non-git dir> list (root-form): clean error, no silent fallback to A (RED, ticket #14)' => sub {
my $no_git = tempdir( CLEANUP => 1 ); # exists, deliberately never git-inited
my $rv = _run_karr( $A, '--dir', $no_git, 'list', '--compact' );
# Probed today: exit 0, silently prints A's own list -- same dangerous
# silent-fallback shape as the nonexistent-path case above.
isnt( $rv->{exit}, 0, '--dir <non-git dir> list exits non-zero' );
like( $rv->{stderr}, qr/not a git repository/i, 'stderr gives a clean "not a git repository" diagnostic' );
unlike( $rv->{stdout}, qr/A-Only Seed Task/, 'stdout shows nothing from board A' );
};
# ------------------------------------------------------------- ticket #15:
# bare `karr --dir PATH` (root-form, NO subcommand, space-separated value)
# dies "Unknown command: PATH" (exit 2) instead of rendering that board.
#
# ROOT CAUSE: the unknown-command guard in App::karr::execute (added for
# ticket #5, see t/41-cli-error-exits.t) does a raw
# `grep { !/^-/ } @$args_ref` over the leftover argv MooX::Cmd hands to
# execute() when nothing dispatched. That grep cannot tell a genuine leftover
# bare word (an actual unknown subcommand) apart from the already-parsed
# --dir value MooX::Cmd echoes back as a bare token in space form -- so
# `karr --dir /path/to/B` trips the guard on "/path/to/B" itself, even though
( run in 0.475 second using v1.01-cache-2.11-cpan-4ef0a570458 )