App-karr

 view release on metacpan or  search on metacpan

t/51-json-output.t  view on Meta::CPAN

    $@;
  };
  return ( $err, $out );
}

subtest 'Task->to_json_hash: frontmatter plus body only when body is set' => sub {
  my $with = App::karr::Task->new( id => 1, title => 'Alpha', body => 'hello' );
  my $h    = $with->to_json_hash;
  is( $h->{id},    1,       'id carried through from frontmatter' );
  is( $h->{title}, 'Alpha', 'title carried through from frontmatter' );
  is( $h->{body},  'hello', 'body key present when body is non-empty' );

  my $without = App::karr::Task->new( id => 2, title => 'Beta' );
  my $h2      = $without->to_json_hash;
  is( $h2->{id}, 2, 'frontmatter still built when body is empty' );
  ok( !exists $h2->{body}, 'body key absent when body is empty' );
};

subtest 'move --json: single id is a bare object, batch is an array' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => 'Alpha', status => 'todo' );

  my $cmd = App::karr::Cmd::Move->new( store => $store, json => 1 );
  my ( $err, $out ) = _run_execute( $cmd, '1', 'done' );
  is( $err, '', 'move 1 done --json does not die' ) or diag $err;

  my $data = eval { decode_json($out) };
  is( ref $data, 'HASH', 'single move --json emits a bare JSON object' ) or diag $out;
  is( $data->{id},         1,      'object carries the moved id' );
  is( $data->{old_status}, 'todo', 'old_status reported' );
  is( $data->{new_status}, 'done', 'new_status reported' );

  my $store2 = _fresh_store();
  _save( $store2, id => 1, title => 'Alpha', status => 'todo' );
  _save( $store2, id => 2, title => 'Beta',  status => 'todo' );

  my $cmd2 = App::karr::Cmd::Move->new( store => $store2, json => 1 );
  my ( $err2, $out2 ) = _run_execute( $cmd2, '1,2', 'done' );
  is( $err2, '', 'move 1,2 done --json does not die' ) or diag $err2;

  my $arr = eval { decode_json($out2) };
  is( ref $arr,       'ARRAY', 'batch move --json emits a JSON array' ) or diag $out2;
  is( scalar @$arr,   2,       'one array entry per moved task' );
  is( $arr->[0]{id},  1,       'first entry is task 1' );
  is( $arr->[1]{id},  2,       'second entry is task 2' );
};

subtest 'move without --json: print_json_results is a no-op (guard)' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => 'Alpha', status => 'todo' );

  my $cmd = App::karr::Cmd::Move->new( store => $store );
  my ( $err, $out ) = _run_execute( $cmd, '1', 'done' );
  is( $err, '', 'move without --json does not die' );
  like( $out, qr/Moved task 1/, 'human-readable line printed' );

  # A removed guard would leak the results object/array into plain output; the
  # human line itself carries no braces, so any brace means JSON leaked through.
  unlike( $out, qr/[{}]/, 'no JSON emitted when --json is absent' );

  my $decoded = eval { decode_json($out) };
  ok( !defined $decoded, 'plain output is not JSON-decodable' );
};

subtest 'edit --json: single id is a bare object with id and title' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => 'Old title', status => 'todo' );

  my $cmd = App::karr::Cmd::Edit->new(
    store => $store,
    json  => 1,
    title => $TITLE,
  );
  my ( $err, $out ) = _run_execute( $cmd, '1' );
  is( $err, '', 'edit 1 --json does not die' ) or diag $err;

  my $data = eval { decode_json($out) };
  is( ref $data,     'HASH', 'single edit --json emits a bare JSON object' ) or diag $out;
  is( $data->{id},    1,     'object carries the edited id' );
  is( $data->{title}, $TITLE, 'edited title reflected in the payload' );
};

subtest 'show --json: explicit id renders frontmatter + body via to_json_hash' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => $TITLE, status => 'todo', body => $BODY );

  my $cmd = App::karr::Cmd::Show->new( store => $store, json => 1 );
  my ( $err, $out ) = _run_execute( $cmd, '1' );
  is( $err, '', 'show 1 --json does not die' ) or diag $err;

  my $data = eval { decode_json($out) };
  is( ref $data,      'HASH',  'explicit id --json stays a bare object' ) or diag $out;
  is( $data->{id},     1,      'id present' );
  is( $data->{title},  $TITLE, 'title present' );
  is( $data->{status}, 'todo', 'status present' );
  is( $data->{body},   $BODY,  'body included when present' );
};

subtest 'pick --json: picked task payload built via to_json_hash' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => 'Alpha', status => 'todo', priority => 'high', body => 'Alpha body' );

  my $cmd = App::karr::Cmd::Pick->new(
    store => $store,
    claim => 'agent-test',
    json  => 1,
  );
  my ( $err, $out ) = _run_execute($cmd);
  is( $err, '', 'pick --json does not die' ) or diag $err;

  my $data = eval { decode_json($out) };
  is( ref $data,          'HASH',       'pick --json emits a bare JSON object' ) or diag $out;
  is( $data->{id},         1,           'picked id present' );
  is( $data->{claimed_by}, 'agent-test', 'claim reflected in payload' );
  is( $data->{body},       'Alpha body', 'body included' );
};

subtest 'handoff --json: review payload built via to_json_hash' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => 'Alpha', status => 'in-progress', body => 'Alpha body' );

  my $cmd = App::karr::Cmd::Handoff->new(
    store => $store,
    claim => 'agent-test',
    json  => 1,
  );
  my ( $err, $out ) = _run_execute( $cmd, '1' );
  is( $err, '', 'handoff --json does not die' ) or diag $err;

  my $data = eval { decode_json($out) };
  is( ref $data,          'HASH',       'handoff --json emits a bare JSON object' ) or diag $out;
  is( $data->{id},         1,           'handoff id present' );
  is( $data->{status},     'review',    'status moved to review' );
  is( $data->{claimed_by}, 'agent-test', 'claim reflected in payload' );
  is( $data->{body},       'Alpha body', 'body included' );
};

subtest 'print_json writes UTF-8 to stdout encoded exactly once' => sub {
  my $store = _fresh_store();
  _save( $store, id => 1, title => $TITLE, status => 'todo', body => $BODY );

  my $cmd = App::karr::Cmd::Show->new( store => $store, json => 1 );
  my ( $err, $out ) = _run_execute( $cmd, '1' );
  is( $err, '', 'show 1 --json does not die' ) or diag $err;

  # decode_json($out) eq $TITLE cannot distinguish a correct encoder from a
  # consistently wrong one, so assert on the octets themselves. Under #53's
  # double encode the first index is -1 and the second is 0.
  ok( index( $out, encode_utf8($TITLE) ) >= 0,
    'the title reaches stdout as singly-encoded UTF-8' )
    or diag unpack( 'H*', $out );
  is( index( $out, encode_utf8( encode_utf8($TITLE) ) ), -1,
    'and never as the double-encoded form' );
  ok( index( $out, encode_utf8($BODY) ) >= 0, 'same for the body' );

  my $decoded = eval { decode( 'UTF-8', $out, FB_CROAK | LEAVE_SRC ) };
  ok( defined $decoded, 'the whole payload is valid UTF-8' );

  my $data = eval { decode_json($out) };
  is( $data->{title}, $TITLE, 'and it still parses back to the characters that went in' );
  is( $data->{body},  $BODY,  'body round-trips too' );
};

done_testing;



( run in 1.634 second using v1.01-cache-2.11-cpan-364913b4093 )