OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / cli-help.test.js

64 lines · javascript ref:
// pgh CLI help/usage surface (forge issue #63 — "Fix pgh ... --help /
// missing-args usage"). Locks in: every command exports a usage string,
// --help is detected before any daemon connection, git-passthrough commands
// forward a trailing --help to git, and `pgh issue <verb>` (a habit-mistake
// where an action is typed in the <repo> slot) surfaces usage rather than
// silently listing a repo by that name.

import test from 'brittle'
import { wantsHelp } from '../lib/cli.js'
import { run as runIssue } from '../lib/commands/issue.js'

// Every command module the dispatcher can load. Kept in sync with the
// COMMANDS map in cli.js (names.js is reached via claim/release/names).
const COMMAND_MODULES = [
  'init', 'store', 'publish', 'releases', 'app', 'seed', 'names', 'user',
  'overlay', 'identity', 'device', 'peer', 'status', 'dump-keys', 'search',
  'profile', 'deploy', 'label', 'admin', 'repo', 'issue', 'org', 'config',
  'commit', 'clone', 'pull', 'push', 'remote', 'server'
]

test('every command module exports a non-empty usage string', async (t) => {
  for (const name of COMMAND_MODULES) {
    const mod = await import(`../lib/commands/${name}.js`)
    t.ok(typeof mod.usage === 'string' && mod.usage.trim().length > 0,
      `${name}.js exports usage`)
  }
})

test('wantsHelp: --help / -h / help anywhere triggers for normal commands', (t) => {
  t.ok(wantsHelp('issue', ['--help']))
  t.ok(wantsHelp('issue', ['-h']))
  t.ok(wantsHelp('issue', ['help']))
  t.ok(wantsHelp('issue', ['myrepo', '--help']), 'non-leading --help still triggers')
  t.absent(wantsHelp('issue', ['myrepo']), 'no help flag → run normally')
  t.absent(wantsHelp('issue', []), 'no args → run normally (lets the command print its own usage)')
})

test('wantsHelp: passthrough commands forward a trailing --help to git', (t) => {
  // Leading help is ours (explain pgh's wrapper)...
  t.ok(wantsHelp('push', ['--help']))
  t.ok(wantsHelp('push', ['help']))
  // ...but a --help after an arg belongs to git.
  t.absent(wantsHelp('push', ['main', '--help']))
  t.absent(wantsHelp('commit', ['-m', 'x', '--help']))
  t.absent(wantsHelp('pull', ['origin', 'main', '-h']))
})

test('pgh issue <action-verb> shows usage instead of listing a repo by that name', async (t) => {
  const client = {
    async call () { t.fail('should not reach the daemon for a mistaken verb'); return {} }
  }
  for (const verb of ['create', 'close', 'comment', 'reopen', 'delete', 'edit']) {
    await t.exception(() => runIssue(client, [verb]),
      new RegExp(`'${verb}' is an action, not a repo`))
  }
})

test('pgh issue <repo> --create dispatches create-issue (regression)', async (t) => {
  const calls = []
  const client = { async call (m, p) { calls.push({ m, p }); return { ok: true } } }
  await runIssue(client, ['myrepo', '--create', 'a title', '--body', 'hello'])
  t.alike(calls, [{ m: 'create-issue', p: { repo: 'myrepo', title: 'a title', body: 'hello' } }])
})