Files
LocalAI/addons/github/ui/test/github.test.mjs
T
Ettore Di Giacinto 6abe5b1882 feat(github): rework the connection page of the GitHub add-on
Token help now works: a button opens GitHub's fine-grained token page,
the steps are open while there is no connection, and a table lists the
permissions for read only and for write modes. The page says it does not
answer mentions yet.

Errors sit under their own field. The repository list splits on lines and
commas only, so "not a repo" is one entry and is named in the message.
The connection name defaults to the first repository.

The write mode cards each carry one consequence line, and "Allowed" is
now "Acts without asking". The Test result is drawn per repository in the
row, with a tone, a busy line, and a plain text when GitHub refuses the
token. The unit tests and a browser spec cover the page.

Assisted-by: Claude:claude-sonnet-5-5 node playwright eslint
2026-11-03 21:43:57 +00:00

126 lines
7.5 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { checkFailure, checkItem, checkLine, defaultName, extraBody, extraForm, extraValid, followName, githubSecret, parseRepos, PERMISSIONS, reposIssue, reposProblem, tokenProblem, WRITE_MODES, writeModeOf } from '../src/repos.js'
import { canSave, connectorPayload, emptyConnector } from '../../../internal/connector-ui/src/connector.js'
import en from '../src/i18n/github.en.json' with { type: 'json' }
test('github: only a fine-grained token is taken, and nothing typed keeps the saved one', () => {
assert.equal(tokenProblem({}), '')
assert.equal(tokenProblem({ token: 'github_pat_abc' }), '')
assert.equal(tokenProblem({ token: 'ghp_classic' }), 'tokenShape')
assert.equal(tokenProblem({ token: 'github_pat_a b' }), 'tokenSpace')
assert.equal(githubSecret({ token: ' github_pat_abc ' }), 'github_pat_abc')
assert.equal(githubSecret({ token: 'ghp_classic' }), '')
})
test('github: the repository list is read from lines and commas, never from spaces, without repeats', () => {
assert.deepEqual(parseRepos('acme/widgets\n Acme/Tools, acme/widgets\r\nx/y'), ['acme/widgets', 'Acme/Tools', 'x/y'])
assert.deepEqual(parseRepos('not a repo'), ['not a repo'], 'a space does not split an entry')
assert.deepEqual(parseRepos(''), [])
assert.equal(reposProblem('acme/widgets\nx/y'), '')
assert.equal(reposProblem('widgets'), 'reposShape')
assert.equal(reposProblem('a/b/c'), 'reposShape')
assert.equal(reposProblem('a/../b'), 'reposShape')
assert.equal(reposProblem('acme/widgets x/y'), 'reposShape')
assert.equal(reposProblem(Array.from({ length: 51 }, (_, i) => `o/r${i}`).join('\n')), 'reposMany')
})
test('github: the error names the line that is wrong', () => {
assert.deepEqual(reposIssue('a/b\nnot a repo\nc/d'), { key: 'reposShape', entry: 'not a repo' })
assert.equal(reposIssue('a/b'), null)
assert.ok(reposIssue('x'.repeat(200)).entry.length <= 60)
})
test('github: the name follows the first repository until the person types one', () => {
assert.equal(defaultName('acme/widgets\nx/y'), 'GitHub: acme/widgets')
assert.equal(defaultName('nope'), '')
assert.equal(followName('', '', 'acme/widgets'), 'GitHub: acme/widgets')
assert.equal(followName('GitHub: acme/widgets', 'acme/widgets', 'acme/tools\nacme/widgets'), 'GitHub: acme/tools')
assert.equal(followName('Docs', 'acme/widgets', 'acme/tools'), 'Docs', 'a typed name stays')
})
test('github: the list rides in the request, a binding is never switched on, and a saved one reads back', () => {
const form = { ...emptyConnector(), name: 'Work', agent_id: 'a', token: 'github_pat_x', ...extraForm({ resources: ['acme/widgets'] }) }
assert.equal(form.repos_text, 'acme/widgets')
const body = connectorPayload(form, 'github', githubSecret(form), { enabled: false, accept_any: false, ...extraBody({ ...form, repos_text: 'acme/widgets\nacme/tools' }) })
assert.equal(body.kind, 'github')
assert.equal(body.token, 'github_pat_x')
assert.equal(body.enabled, false)
assert.deepEqual(body.resources, ['acme/widgets', 'acme/tools'])
const update = connectorPayload({ ...emptyConnector(), id: 'b1', name: 'Work', agent_id: 'a' }, 'github', '', extraBody({ repos_text: '' }))
assert.equal('token' in update, false)
assert.deepEqual(update.resources, [], 'an empty list clears the list')
assert.equal(canSave(form, false, Boolean(githubSecret(form))), true)
assert.equal(extraValid({ repos_text: 'nope' }), false)
})
test('github: the Test says reachable, read only, not reachable, refused or unfinished, each with a tone', () => {
const t = (key, o = {}) => key.split('.').reduce((n, k) => n[k], en).replace(/\{\{(\w+)\}\}/g, (_, k) => o[k])
assert.match(checkLine({ repo: 'a/b', reachable: false }, t), /cannot reach/)
assert.match(checkLine({ repo: 'a/b', reachable: true, fullName: 'a/b', canWrite: false }, t), /read only/)
assert.match(checkLine({ repo: 'a/b', reachable: true, fullName: 'a/b', canWrite: true }, t), /can write/)
assert.match(checkLine({ repo: 'a/b', failed: 'refused' }, t), /refused this token.*expired/)
assert.equal(checkItem({ repo: 'a/b', reachable: true }).tone, 'ok')
assert.equal(checkItem({ repo: 'a/b', reachable: false }).tone, 'warn')
assert.equal(checkItem({ repo: 'a/b', failed: 'other' }).tone, 'warn')
assert.equal(checkFailure({ status: 401 }), 'refused')
assert.equal(checkFailure(new Error('Bad credentials')), 'refused')
assert.equal(checkFailure(new Error('timeout')), 'other')
})
test('github: the permission table covers the permissions the steps name', () => {
assert.deepEqual(PERMISSIONS.map((p) => p.id), ['contents', 'issues', 'pulls', 'metadata'])
for (const p of PERMISSIONS) {
assert.ok(en.setup.table[p.id], p.id)
assert.ok(en.setup.table.levels[p.read] && en.setup.table.levels[p.write], p.id)
}
assert.equal(PERMISSIONS.find((p) => p.id === 'issues').write, 'readwrite')
assert.equal(PERMISSIONS.find((p) => p.id === 'contents').write, 'read', 'writing never needs to push code')
})
test('github: every word the page asks for exists', () => {
for (const key of ['title', 'intro', 'new', 'token', 'tokenHelp', 'tokenShape', 'tokenSpace', 'repos', 'reposHelp', 'reposShape', 'reposMany', 'writeMode', 'test', 'testOk', 'save', 'cancel', 'empty', 'noRepos', 'reposOf', 'agentOf', 'confirmDelete', 'setup', 'limits', 'approval', 'check']) {
assert.ok(en[key], key)
}
})
test('github: the write mode defaults to off, rides in the request and reads back from a saved binding', () => {
assert.deepEqual(WRITE_MODES, ['off', 'propose', 'auto'])
assert.equal(writeModeOf(undefined), 'off')
assert.equal(writeModeOf('always'), 'off', 'an unknown value is never taken as a permission')
assert.equal(extraForm({}).write_mode, 'off')
assert.equal(extraForm({ write_mode: 'propose' }).write_mode, 'propose')
assert.equal(extraBody({ repos_text: 'a/b', write_mode: 'auto' }).write_mode, 'auto')
assert.equal(extraBody({ repos_text: 'a/b' }).write_mode, 'off', 'a form without the field sends off')
const body = connectorPayload({ ...emptyConnector(), id: 'b1', name: 'Work', agent_id: 'a' }, 'github', '', extraBody({ repos_text: 'a/b', write_mode: 'propose' }))
assert.equal(body.write_mode, 'propose')
})
test('github: every write mode has its words, and the note says the text is public', () => {
for (const m of WRITE_MODES) {
assert.ok(en.writeMode[m], m)
assert.ok(en.writeMode.note[m], m)
assert.ok(en.writeMode.row[m], m)
}
assert.match(en.writeMode.note.propose, /public/)
assert.match(en.writeMode.note.auto, /public/)
})
test('github: a connection can be saved without an agent, and the page has words for it', () => {
const form = { ...emptyConnector(), name: 'Work', token: 'github_pat_x' }
assert.equal(canSave(form, false, true), false, 'a kind that needs an agent refuses an empty one')
assert.equal(canSave(form, false, true, null, true), true, 'agentOptional lets the connection hold credentials only')
assert.equal(connectorPayload(form, 'github', 'github_pat_x', { enabled: false }).agent_id, '')
for (const key of ['agentCredentialsOnly', 'agentOptionalHint', 'credentialsOnly']) assert.ok(en[key], key)
})
test('github: the page says what it does not do and links the token page', () => {
assert.match(en.limits, /does not answer @mentions/)
assert.match(en.setup.linkLabel, /new tab/)
})
test('github: every write mode has a title and a consequence line', () => {
for (const m of WRITE_MODES) assert.ok(en.writeMode.line[m], m)
assert.notEqual(en.writeMode.auto, 'Allowed')
})