mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(addons): GitHub settings page, registration and docs
Add the GitHub connection page (fine-grained token, repository list, read-only note, Test that checks each listed repository), let the shared connector page hide messaging settings for a kind that carries no messages, register the add-on in the package matrix and the catalog, and document the credential slot and the GitHub add-on. Assisted-by: Claude:claude-sonnet-5-5 [go node]
This commit is contained in:
19 files changed
+557
-24
No files matched your search
@@ -0,0 +1,52 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { checkLine, extraBody, extraForm, extraValid, githubSecret, parseRepos, reposProblem, tokenProblem } 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, commas and spaces without repeats', () => {
|
||||
assert.deepEqual(parseRepos('acme/widgets\n Acme/Tools, acme/widgets x/y'), ['acme/widgets', 'Acme/Tools', 'x/y'])
|
||||
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(Array.from({ length: 51 }, (_, i) => `o/r${i}`).join('\n')), 'reposMany')
|
||||
})
|
||||
|
||||
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 lines say reachable, read only, or not reachable', () => {
|
||||
const t = (key, o = {}) => en.check[key.replace('check.', '')].replace(/\{\{(\w+)\}\}/g, (_, k) => o[k])
|
||||
assert.match(checkLine({ repo: 'a/b', reachable: false }, t), /not reachable/)
|
||||
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), /read access only/)
|
||||
})
|
||||
|
||||
test('github: every word the page asks for exists', () => {
|
||||
for (const key of ['title', 'intro', 'new', 'token', 'tokenHelp', 'tokenShape', 'tokenSpace', 'repos', 'reposHelp', 'reposShape', 'reposMany', 'writeOff', 'test', 'testOk', 'save', 'cancel', 'empty', 'noRepos', 'reposOf', 'agentOf', 'confirmDelete', 'setup']) {
|
||||
assert.ok(en[key], key)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
// Runs the node unit tests and prints one JSON line per test:
|
||||
// {"case":"unit: <name>","status":"passed"|"failed"|"skipped"}
|
||||
// A test that is skipped or marked todo is reported as skipped, so the case
|
||||
// inventory of scripts/addons/suites.json rejects it. No dependencies.
|
||||
//
|
||||
// Arguments name the test files to run (for example addons.test.mjs); without
|
||||
// any, every *.test.mjs file runs. Each suite of suites.json names its own, so
|
||||
// one suite's cases never appear in another's inventory.
|
||||
import { run } from 'node:test'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const only = process.argv.slice(2)
|
||||
const names = readdirSync(here).filter(f => f.endsWith('.test.mjs')).sort()
|
||||
for (const name of only) {
|
||||
if (!names.includes(name)) {
|
||||
console.log(JSON.stringify({ case: `unit: missing test file ${name}`, status: 'failed' }))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
const files = names.filter(f => only.length === 0 || only.includes(f)).map(f => join(here, f))
|
||||
let failed = false
|
||||
const stream = run({ files, concurrency: 1 })
|
||||
stream.on('test:pass', ({ name, skip, todo, nesting }) => {
|
||||
if (nesting > 0) return
|
||||
const status = skip || todo ? 'skipped' : 'passed'
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status }))
|
||||
})
|
||||
stream.on('test:fail', ({ name, nesting, details }) => {
|
||||
if (nesting > 0) return
|
||||
failed = true
|
||||
console.log(JSON.stringify({ case: `unit: ${name}`, status: 'failed', error: String(details?.error?.message || '').slice(0, 300) }))
|
||||
})
|
||||
stream.on('test:stderr', ({ message }) => process.stderr.write(message))
|
||||
stream.on('end', () => { process.exitCode = failed ? 1 : 0 })
|
||||
stream.resume()
|
||||
Reference in new issue
Block a user