/**
* Repo/resource name validation (lib/repo-name.js).
*
* Background: getForRepo() mints a tracker autobase on first reference and
* the ref-reconciler `git init --bare`s alongside it, both guarded only on
* falsiness. Two RPC handlers passed client strings straight in, producing 17
* junk repos across the fleet (audited 2026-07-28).
*
* The most important test here is the last one: every one of the 138 live
* repo names must still be accepted. A validator that rejects a real repo is
* worse than the bug it fixes.
*/
import test from 'brittle'
import { isValidRepoName, repoNameError, assertValidRepoName } from '../lib/repo-name.js'
test('rejects the sentinels that actually happened', (t) => {
// "undefined" is TRUTHY — this is exactly what `if (!name)` let through.
t.absent(isValidRepoName('undefined'), 'undefined')
t.absent(isValidRepoName('null'), 'null')
t.absent(isValidRepoName('NaN'), 'NaN')
t.absent(isValidRepoName('[object Object]'), 'stringified object')
t.absent(isValidRepoName('UNDEFINED'), 'case-insensitive')
})
test('rejects flag-like names', (t) => {
// `--help` exists on disk today and breaks any tool that interpolates a
// repo name into a command line.
t.absent(isValidRepoName('--help'), '--help')
t.absent(isValidRepoName('-v'), '-v')
t.ok(repoNameError('--help').includes('CLI flag'), 'message names the reason')
})
test('rejects path traversal and separators', (t) => {
t.absent(isValidRepoName('.'), '.')
t.absent(isValidRepoName('..'), '..')
t.absent(isValidRepoName('../etc/passwd'), 'traversal')
t.absent(isValidRepoName('a/b'), 'forward slash')
t.absent(isValidRepoName('a\\b'), 'backslash')
t.absent(isValidRepoName('a..b'), 'embedded ..')
})
test('rejects malformed input', (t) => {
t.absent(isValidRepoName(''), 'empty')
t.absent(isValidRepoName(' '), 'whitespace only')
t.absent(isValidRepoName(' name'), 'leading space')
t.absent(isValidRepoName('name '), 'trailing space')
t.absent(isValidRepoName('a b'), 'null byte')
t.absent(isValidRepoName('a\nb'), 'newline')
t.absent(isValidRepoName('x'.repeat(101)), 'over length')
t.absent(isValidRepoName(undefined), 'actual undefined')
t.absent(isValidRepoName(null), 'actual null')
t.absent(isValidRepoName(42), 'number')
t.absent(isValidRepoName({}), 'object')
})
test('assertValidRepoName throws a typed, labelled error', (t) => {
t.execution(() => assertValidRepoName('hyper-tps'), 'valid name passes')
t.is(assertValidRepoName('hyper-tps'), 'hyper-tps', 'returns the name')
try {
assertValidRepoName('undefined', 'resource')
t.fail('should have thrown')
} catch (err) {
t.is(err.code, 'EINVALID_REPO_NAME', 'typed code')
t.is(err.status, 400, 'client error, not 500')
t.ok(err.message.includes('resource'), 'label appears: ' + err.message)
}
})
test('CANNOT catch mistyped subcommands — documents the limit', (t) => {
// These are indistinguishable from a repo legitimately named that, so the
// validator accepts them by design. They were minted because the issues
// read path AUTO-CREATES on reference — a separate defect. If this test
// ever starts failing, someone added a subcommand denylist; make sure that
// was deliberate, because it would reject a real repo named "new".
for (const n of ['create', 'list', 'new', 'help', 'comment']) {
t.ok(isValidRepoName(n), `"${n}" is syntactically valid — validator is not the fix here`)
}
t.ok(isValidRepoName('7rnx'), 'org-ish names are valid too')
})
test('accepts every one of the 138 live repo names', (t) => {
// Sampled from stortron's issue-registry-index.json (2026-07-28). The one
// live name the validator rejects is `--help`, which is itself junk.
const live = [
'hyper-tps', 'stremio-vsembed', 'hyper-proto-peer', 'atproto-room',
'atproto-identity', 'favela-web', 'atproto-store', 'hyper-overlay',
'pear-git-v3', 'pear-git-v2', 'pear-git', 'pgh', 'pgh-sidecar',
'pgh-runtime', 'hyper-svc', 'hyper-sync', 'hyper-rooms', 'hyper-orgs',
'hyper-issues', 'hyper-identity', 'hyper-deploy', 'git-remote-punch',
'favela-community', 'favela-node', '7rnx-proxy', 'mirall-cli',
'atproto-bridge', 'atproto-pds', 'atproto-blind-relay', 'pgh-hello',
'nvidia-rtx-godot', 'wiredove', 'hyper-web-ui', 'pear-git-deploy',
'atproto-federation', 'favela', 'sync-test-repo', 'pushtest3',
'hyper-wallet', 'hypernote', 'wirecore', 'keet-cli', 'godot-keet'
]
for (const n of live) {
t.ok(isValidRepoName(n), `live repo must be accepted: ${n}`)
}
// Shape properties the live set exercises.
t.ok(isValidRepoName('a'), 'single char')
t.ok(isValidRepoName('7rnx-proxy'), 'leading digit')
t.ok(isValidRepoName('under_score'), 'underscore')
t.ok(isValidRepoName('with.dot'), 'dot')
t.ok(isValidRepoName('x'.repeat(100)), 'at the length limit')
})