3042 lines
96 KiB
JavaScript
3042 lines
96 KiB
JavaScript
import test from 'brittle'
|
|
import {
|
|
BARE_OS_DISCORD_SERVICE_ENV,
|
|
BARE_OS_DISCORD_UNIT,
|
|
applyDiscordExtrasToSession,
|
|
bareOsDiscordInitdEnabled,
|
|
hydrateBareOsDiscordSessionEnv,
|
|
readBareOsDiscordServiceEnv,
|
|
syncBareOsDiscordInitd,
|
|
stopBareOsDiscordInitd
|
|
} from './lib/bare-os-discord-initd.js'
|
|
import {
|
|
findBareServiceDefinition,
|
|
listBareServices,
|
|
unregisterBareService
|
|
} from './lib/bare-initd.js'
|
|
import { createRequire } from 'node:module'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
function makeCtx(files, env) {
|
|
return {
|
|
env: env || {},
|
|
b4a: { toString: (b) => Buffer.from(b).toString('utf8') },
|
|
console: { log() {}, error() {} },
|
|
vfs: {
|
|
readFile: async (p) => {
|
|
const t = files[p]
|
|
if (t == null) {
|
|
const e = new Error('ENOENT')
|
|
e.code = 'ENOENT'
|
|
throw e
|
|
}
|
|
return Buffer.from(t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test('bareOsDiscordInitdEnabled honors BARE_OS_DISCORD_INITD=0', (t) => {
|
|
t.ok(bareOsDiscordInitdEnabled({}))
|
|
t.absent(bareOsDiscordInitdEnabled({ BARE_OS_DISCORD_INITD: '0' }))
|
|
t.absent(bareOsDiscordInitdEnabled({ BARE_OS_DISCORD: 'false' }))
|
|
})
|
|
|
|
test('readBareOsDiscordServiceEnv requires DISCORD_TOKEN in ~/.discord/.env', async (t) => {
|
|
t.is(BARE_OS_DISCORD_SERVICE_ENV, '~/.discord/.env')
|
|
t.is(await readBareOsDiscordServiceEnv(makeCtx({}, {})), null)
|
|
t.is(
|
|
await readBareOsDiscordServiceEnv(
|
|
makeCtx({ '~/.discord/.env': 'FOO=bar\n' }, {})
|
|
),
|
|
null
|
|
)
|
|
const ok = await readBareOsDiscordServiceEnv(
|
|
makeCtx(
|
|
{
|
|
'~/.discord/.env':
|
|
'DISCORD_TOKEN=abc.def\nDISCORD_GUILD_ID=99\nDISCORD_ID_WHITELIST=111, 222\n'
|
|
},
|
|
{}
|
|
)
|
|
)
|
|
t.ok(ok)
|
|
t.is(ok.token, 'abc.def')
|
|
t.is(ok.guildId, '99')
|
|
t.is(ok.whitelist, '111, 222')
|
|
t.is(ok.userInstall, '')
|
|
const withUi = await readBareOsDiscordServiceEnv(
|
|
makeCtx(
|
|
{
|
|
'~/.discord/.env':
|
|
'DISCORD_TOKEN=abc.def\nDISCORD_USER_INSTALL=0\n'
|
|
},
|
|
{}
|
|
)
|
|
)
|
|
t.is(withUi.userInstall, '0')
|
|
})
|
|
|
|
test('hydrateBareOsDiscordSessionEnv copies whitelist onto both env maps', async (t) => {
|
|
const snow = '342128351638585344'
|
|
const vfsEnv = {}
|
|
const ctx = makeCtx(
|
|
{
|
|
'~/.discord/.env': 'DISCORD_TOKEN=abc.def\nDISCORD_ID_WHITELIST=' + snow + '\n'
|
|
},
|
|
{}
|
|
)
|
|
ctx.vfs.env = vfsEnv
|
|
const creds = await hydrateBareOsDiscordSessionEnv(ctx)
|
|
t.ok(creds)
|
|
t.is(creds.whitelist, snow)
|
|
t.is(ctx.env.DISCORD_ID_WHITELIST, snow)
|
|
t.is(vfsEnv.DISCORD_ID_WHITELIST, snow)
|
|
})
|
|
|
|
test('hydrateBareOsDiscordSessionEnv reads DISCORD_ID_WHITELIST from ~/.env', async (t) => {
|
|
const snow = '342128351638585344'
|
|
const vfsEnv = {}
|
|
const ctx = makeCtx(
|
|
{
|
|
'~/.discord/.env': 'DISCORD_TOKEN=abc.def\n',
|
|
'~/.env': 'DISCORD_ID_WHITELIST=' + snow + '\n'
|
|
},
|
|
{}
|
|
)
|
|
ctx.vfs.env = vfsEnv
|
|
const creds = await hydrateBareOsDiscordSessionEnv(ctx)
|
|
t.ok(creds)
|
|
t.is(creds.whitelist, snow)
|
|
t.is(ctx.env.DISCORD_ID_WHITELIST, snow)
|
|
t.is(vfsEnv.DISCORD_ID_WHITELIST, snow)
|
|
})
|
|
|
|
test('applyDiscordExtrasToSession does not let empty vfs.env hide a file whitelist', (t) => {
|
|
const ctx = {
|
|
env: {},
|
|
vfs: { env: {} }
|
|
}
|
|
applyDiscordExtrasToSession(ctx, {
|
|
DISCORD_ID_WHITELIST: '342128351638585344'
|
|
})
|
|
t.is(ctx.env.DISCORD_ID_WHITELIST, '342128351638585344')
|
|
t.is(ctx.vfs.env.DISCORD_ID_WHITELIST, '342128351638585344')
|
|
})
|
|
|
|
test('syncBareOsDiscordInitd hides unit without env file', async (t) => {
|
|
unregisterBareService(BARE_OS_DISCORD_UNIT)
|
|
const hidden = makeCtx({}, {})
|
|
t.absent(await syncBareOsDiscordInitd(hidden))
|
|
t.absent(findBareServiceDefinition(BARE_OS_DISCORD_UNIT))
|
|
t.absent(listBareServices().some((s) => s.name === BARE_OS_DISCORD_UNIT))
|
|
|
|
const shown = makeCtx({ '~/.discord/.env': 'DISCORD_TOKEN=tok.en\n' }, {})
|
|
t.ok(await syncBareOsDiscordInitd(shown))
|
|
t.ok(findBareServiceDefinition(BARE_OS_DISCORD_UNIT))
|
|
t.ok(listBareServices().some((s) => s.name === BARE_OS_DISCORD_UNIT))
|
|
|
|
await stopBareOsDiscordInitd(shown)
|
|
t.absent(findBareServiceDefinition(BARE_OS_DISCORD_UNIT))
|
|
t.absent(listBareServices().some((s) => s.name === BARE_OS_DISCORD_UNIT))
|
|
})
|
|
|
|
function mockSlash() {
|
|
function B() {
|
|
this.json = { name: '', description: '', options: [] }
|
|
}
|
|
B.prototype.setName = function (n) {
|
|
this.json.name = n
|
|
return this
|
|
}
|
|
B.prototype.setDescription = function (d) {
|
|
this.json.description = d
|
|
return this
|
|
}
|
|
B.prototype.addSubcommand = function (fn) {
|
|
const s = new B()
|
|
function attachString(ofn) {
|
|
const o = {
|
|
name: '',
|
|
description: '',
|
|
required: false,
|
|
min_length: 0,
|
|
max_length: 0,
|
|
setName(n) {
|
|
this.name = n
|
|
return this
|
|
},
|
|
setDescription(d) {
|
|
this.description = d
|
|
return this
|
|
},
|
|
setRequired(v) {
|
|
this.required = v !== false
|
|
return this
|
|
},
|
|
setMinLength(n) {
|
|
this.min_length = n
|
|
return this
|
|
},
|
|
setMaxLength(n) {
|
|
this.max_length = n
|
|
return this
|
|
},
|
|
setAutocomplete() {
|
|
return this
|
|
}
|
|
}
|
|
ofn(o)
|
|
this.json.options = this.json.options || []
|
|
this.json.options.push({
|
|
type: 3,
|
|
name: o.name,
|
|
description: o.description,
|
|
required: !!o.required,
|
|
min_length: o.min_length || undefined,
|
|
max_length: o.max_length || undefined
|
|
})
|
|
return this
|
|
}
|
|
s.addStringOption = attachString
|
|
s.addBooleanOption = function (ofn) {
|
|
const o = {
|
|
name: '',
|
|
setName(n) {
|
|
this.name = n
|
|
return this
|
|
},
|
|
setDescription() {
|
|
return this
|
|
},
|
|
setRequired() {
|
|
return this
|
|
}
|
|
}
|
|
ofn(o)
|
|
this.json.options = this.json.options || []
|
|
this.json.options.push({ type: 5, name: o.name })
|
|
return this
|
|
}
|
|
s.addIntegerOption = function (ofn) {
|
|
const o = {
|
|
name: '',
|
|
setName(n) {
|
|
this.name = n
|
|
return this
|
|
},
|
|
setDescription() {
|
|
return this
|
|
},
|
|
setRequired() {
|
|
return this
|
|
},
|
|
setMinValue() {
|
|
return this
|
|
},
|
|
setMaxValue() {
|
|
return this
|
|
}
|
|
}
|
|
ofn(o)
|
|
this.json.options = this.json.options || []
|
|
this.json.options.push({ type: 4, name: o.name })
|
|
return this
|
|
}
|
|
fn(s)
|
|
this.json.options.push({
|
|
type: 1,
|
|
name: s.json.name,
|
|
description: s.json.description,
|
|
options: s.json.options || []
|
|
})
|
|
return this
|
|
}
|
|
B.prototype.addStringOption = function (ofn) {
|
|
const o = {
|
|
setName(n) {
|
|
this.name = n
|
|
return this
|
|
},
|
|
setDescription() {
|
|
return this
|
|
},
|
|
setRequired(v) {
|
|
this.required = v !== false
|
|
return this
|
|
}
|
|
}
|
|
ofn(o)
|
|
this.json.options.push({ type: 3, name: o.name, required: !!o.required })
|
|
return this
|
|
}
|
|
B.prototype.addAttachmentOption = function (ofn) {
|
|
const o = {
|
|
setName(n) {
|
|
this.name = n
|
|
return this
|
|
},
|
|
setDescription() {
|
|
return this
|
|
},
|
|
setRequired(v) {
|
|
this.required = v !== false
|
|
return this
|
|
}
|
|
}
|
|
ofn(o)
|
|
this.json.options.push({ type: 11, name: o.name, required: !!o.required })
|
|
return this
|
|
}
|
|
B.prototype.toJSON = function () {
|
|
return this.json
|
|
}
|
|
return B
|
|
}
|
|
|
|
function collectCustomIds(payload) {
|
|
const ids = []
|
|
const rows = (payload && payload.components) || []
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const comps = (rows[i] && rows[i].components) || []
|
|
for (let j = 0; j < comps.length; j++) {
|
|
if (comps[j] && comps[j].custom_id) ids.push(String(comps[j].custom_id))
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
function assertUniqueCustomIds(t, payload, label) {
|
|
const ids = collectCustomIds(payload)
|
|
const seen = Object.create(null)
|
|
for (let i = 0; i < ids.length; i++) {
|
|
t.absent(seen[ids[i]], (label || 'custom_id') + ' unique ' + ids[i])
|
|
seen[ids[i]] = 1
|
|
}
|
|
}
|
|
|
|
test('discord command catalog builds Bare OS slash commands', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
const names = body.map((c) => c.name).sort()
|
|
t.ok(names.indexOf('bare') >= 0)
|
|
t.ok(names.indexOf('sys') >= 0)
|
|
t.ok(names.indexOf('svc') >= 0)
|
|
t.ok(names.indexOf('fs') >= 0)
|
|
t.ok(names.indexOf('net') >= 0)
|
|
t.ok(names.indexOf('ping') >= 0)
|
|
t.ok(names.indexOf('settings') >= 0)
|
|
t.ok(names.indexOf('plugins') >= 0)
|
|
t.ok(names.indexOf('upload') >= 0)
|
|
t.ok(names.indexOf('hdms') >= 0)
|
|
t.ok(names.indexOf('holesail') >= 0)
|
|
t.ok(names.indexOf('agent') >= 0)
|
|
t.ok(body.every((c) => Array.isArray(c.integration_types) && c.integration_types.indexOf(1) >= 0))
|
|
t.ok(body.every((c) => Array.isArray(c.contexts) && c.contexts.length === 3))
|
|
const guildOnly = await cmds.buildSlashCommands(
|
|
{ SlashCommandBuilder: mockSlash() },
|
|
{ env: { DISCORD_USER_INSTALL: '0' } }
|
|
)
|
|
t.ok(Array.isArray(guildOnly) && guildOnly.every((c) => !c.integration_types))
|
|
const uploadCmd = body.find((c) => c.name === 'upload')
|
|
const fileOpt = uploadCmd && (uploadCmd.options || []).find((o) => o.name === 'file')
|
|
t.ok(fileOpt && fileOpt.type === 11 && fileOpt.required, '/upload file attachment is required')
|
|
t.ok(body.length >= 8)
|
|
|
|
const replies = []
|
|
const interaction = {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'bare',
|
|
options: {
|
|
getSubcommand: () => 'help',
|
|
getString: () => ''
|
|
},
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
}
|
|
}
|
|
await cmds.dispatchInteraction({ env: {}, vfs: {}, console: {} }, interaction)
|
|
const helpBlob = JSON.stringify(replies[0] || {})
|
|
t.ok(/\/bare/.test(helpBlob), 'help mentions /bare')
|
|
assertUniqueCustomIds(t, replies[0], 'help')
|
|
})
|
|
|
|
test('Discord message components never reuse a custom_id', (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const kept = cmds.uniqueComponents([
|
|
{
|
|
type: 1,
|
|
components: [
|
|
{ type: 2, custom_id: 'edit:new', label: 'Edit another…' },
|
|
{ type: 2, custom_id: 'create:new', label: 'Create another…' }
|
|
]
|
|
},
|
|
{
|
|
type: 1,
|
|
components: [
|
|
{ type: 2, custom_id: 'edit:new', label: 'Edit file…' },
|
|
{ type: 2, custom_id: 'create:new', label: 'Create file…' },
|
|
{ type: 2, custom_id: 'fm:home', label: 'Files' }
|
|
]
|
|
}
|
|
])
|
|
t.alike(collectCustomIds({ components: kept }), ['edit:new', 'create:new', 'fm:home'])
|
|
|
|
const saved = cmds.result(
|
|
{ title: 'Saved ~/notes.txt', color: 1 },
|
|
{
|
|
components: [
|
|
{
|
|
type: 1,
|
|
components: [
|
|
{ type: 2, custom_id: 'edit:open', label: 'Edit again' },
|
|
{ type: 2, custom_id: 'edit:new', label: 'Edit another…' },
|
|
{ type: 2, custom_id: 'create:new', label: 'Create another…' }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
)
|
|
assertUniqueCustomIds(t, saved, 'save+nav')
|
|
t.ok(collectCustomIds(saved).indexOf('edit:new') >= 0)
|
|
t.ok(collectCustomIds(saved).indexOf('edit:open') >= 0)
|
|
|
|
const panel = cmds.result({ title: 'panel' })
|
|
assertUniqueCustomIds(t, panel, 'panel nav')
|
|
const panelIds = collectCustomIds(panel)
|
|
t.ok(panelIds.indexOf('nav:menu') >= 0, 'collapsed Menu button')
|
|
t.absent(panelIds.indexOf('nav:panel') >= 0, 'full nav hidden until Menu')
|
|
t.ok(panelIds.length <= 2, 'collapsed chrome stays small')
|
|
const payload = cmds.replyPayload(saved)
|
|
assertUniqueCustomIds(t, payload, 'reply payload')
|
|
t.ok(collectCustomIds(payload).indexOf('nav:menu') >= 0)
|
|
})
|
|
|
|
test('Discord Menu button expands and collapses the nav chrome', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const views = []
|
|
const ctx = { env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } }, console: {} }
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'bare',
|
|
options: { getSubcommand: () => 'help', getString: () => '' },
|
|
user: { id: 'nav1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
return { id: 'm-nav', embeds: p.embeds, components: p.components }
|
|
}
|
|
})
|
|
const closed = collectCustomIds(views[0])
|
|
t.ok(closed.indexOf('nav:menu') >= 0, 'help starts with Menu')
|
|
t.absent(closed.indexOf('sys:doctor') >= 0, 'help does not dump Doctor/Services/…')
|
|
t.ok(closed.length <= 3, 'collapsed help is a short row')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => true,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
customId: 'nav:menu',
|
|
user: { id: 'nav1' },
|
|
message: {
|
|
id: 'm-nav',
|
|
embeds: views[0].embeds,
|
|
components: views[0].components,
|
|
interaction: { user: { id: 'nav1' } }
|
|
},
|
|
update: async (p) => {
|
|
views.push(p)
|
|
return { id: 'm-nav', embeds: p.embeds, components: p.components }
|
|
}
|
|
})
|
|
const opened = collectCustomIds(views[1])
|
|
t.ok(opened.indexOf('nav:panel') >= 0, 'Menu reveals Panel')
|
|
t.ok(opened.indexOf('set:home') >= 0, 'Menu reveals Settings')
|
|
t.ok(opened.indexOf('nav:menu') >= 0, 'Hide menu stays available')
|
|
assertUniqueCustomIds(t, views[1], 'expanded nav')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => true,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
customId: 'nav:menu',
|
|
user: { id: 'nav1' },
|
|
message: {
|
|
id: 'm-nav',
|
|
embeds: views[1].embeds,
|
|
components: views[1].components,
|
|
interaction: { user: { id: 'nav1' } }
|
|
},
|
|
update: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const again = collectCustomIds(views[2])
|
|
t.ok(again.indexOf('nav:menu') >= 0, 'Hide menu collapses back')
|
|
t.absent(again.indexOf('nav:panel') >= 0, 'expanded destinations are gone')
|
|
})
|
|
|
|
test('idle timeout deletes stale embeds and falls back to stripping components', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.is(cmds.idleMs, 2 * 60 * 1000)
|
|
|
|
const deleted = []
|
|
const msg = {
|
|
id: 'm-idle',
|
|
delete: async () => {
|
|
deleted.push('del')
|
|
},
|
|
edit: async () => {
|
|
deleted.push('edit')
|
|
}
|
|
}
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } }, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'panel',
|
|
user: { id: 'idle1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async () => msg
|
|
}
|
|
)
|
|
t.ok(cmds.idleLive()['m-idle'], 'tracks posted embed')
|
|
t.ok(await cmds.idleExpire('m-idle'))
|
|
t.is(deleted[0], 'del')
|
|
t.absent(cmds.idleLive()['m-idle'])
|
|
|
|
const edited = []
|
|
const msg2 = {
|
|
id: 'm-edit',
|
|
delete: async () => {
|
|
throw new Error('Missing Permissions')
|
|
},
|
|
edit: async (p) => {
|
|
edited.push(p)
|
|
}
|
|
}
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } }, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'panel',
|
|
user: { id: 'idle2' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async () => msg2
|
|
}
|
|
)
|
|
t.ok(await cmds.idleExpire('m-edit'))
|
|
t.ok(edited[0] && Array.isArray(edited[0].components) && edited[0].components.length === 0)
|
|
t.ok(edited[0].embeds && /Expired after 2 minutes idle/.test(JSON.stringify(edited[0].embeds)))
|
|
})
|
|
|
|
test('embed packer stays inside Discord limits and paginates long text', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.ok(cmds.LIMIT.embedTotal === 6000)
|
|
t.ok(cmds.LIMIT.desc === 4096)
|
|
t.ok(cmds.LIMIT.fieldValue === 1024)
|
|
|
|
const fields = []
|
|
for (let i = 0; i < 25; i++) {
|
|
fields.push({ name: 'Field ' + i, value: Array(80).join('x'), inline: true })
|
|
}
|
|
const packed = cmds.packEmbed({
|
|
title: 'Huge',
|
|
desc: Array(50).join('line of description\n'),
|
|
fields: fields,
|
|
footer: 'footer'
|
|
})
|
|
t.ok(packed.size <= 6000, 'total embed <= 6000')
|
|
t.ok((packed.embed.description || '').length <= 4096)
|
|
t.ok((packed.embed.fields || []).length <= 25)
|
|
t.ok((packed.embed.fields || []).every((f) => String(f.value).length <= 1024))
|
|
|
|
const fenced = cmds.packEmbed({
|
|
title: 'Fence',
|
|
desc: '```\n' + Array(200).join('abcdefghijklmnopqrstuvwxyz\n') + '\n```\nPage **2/4**',
|
|
prefer: 'desc'
|
|
})
|
|
const d = fenced.embed.description || ''
|
|
t.ok(d.length <= 4096)
|
|
t.ok(/```/.test(d), 'keeps a fence')
|
|
t.ok((d.match(/```/g) || []).length % 2 === 0, 'fence stays closed')
|
|
|
|
const pages = cmds.textPages(Array(40).join('hello world\n') + 'end', 80)
|
|
t.ok(pages.length > 1)
|
|
t.ok(pages.join('\n').indexOf('end') >= 0)
|
|
|
|
const big = []
|
|
for (let i = 0; i < 400; i++) big.push('line ' + i + ' lorem ipsum dolor sit amet')
|
|
const views = []
|
|
await cmds.dispatchInteraction(
|
|
{
|
|
env: { USER: 'alice' },
|
|
vfs: {
|
|
env: { USER: 'alice' },
|
|
readFile: async (p) => {
|
|
if (p === '~/big.txt') return Buffer.from(big.join('\n'))
|
|
throw new Error('ENOENT')
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} }
|
|
},
|
|
{
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'fs',
|
|
user: { id: 'overflow1' },
|
|
options: {
|
|
getSubcommand: () => 'cat',
|
|
getString: (k) => (k === 'path' ? '~/big.txt' : '')
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
return { id: 'm-big', delete: async () => {}, edit: async () => {} }
|
|
}
|
|
}
|
|
)
|
|
const first = views[0] || {}
|
|
t.ok(first.embeds && first.embeds[0], 'posted an embed')
|
|
t.ok(cmds.embedSize(first.embeds[0]) <= 6000)
|
|
t.ok(
|
|
first.components &&
|
|
first.components.some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'more:next')
|
|
),
|
|
'long file gets next-page button'
|
|
)
|
|
assertUniqueCustomIds(t, first, 'long cat')
|
|
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } }, console: {} },
|
|
{
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => true,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
customId: 'more:next',
|
|
user: { id: 'overflow1' },
|
|
message: { id: 'm-big' },
|
|
update: async (p) => {
|
|
views.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(/Page \*\*2\//.test(JSON.stringify(views[1] || {})), 'advances page')
|
|
})
|
|
|
|
test('DISCORD_ID_WHITELIST denies users not on the list', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const parsed = cmds.parseIdWhitelist(' 111 ,222, <@333> ')
|
|
t.is(parsed['111'], 1)
|
|
t.is(parsed['222'], 1)
|
|
t.is(parsed['333'], 1)
|
|
t.ok(cmds.userAllowed({ env: {} }, '999'))
|
|
t.ok(cmds.userAllowed({ env: { DISCORD_ID_WHITELIST: '' } }, '999'))
|
|
t.ok(
|
|
cmds.userAllowed({ env: { DISCORD_ID_WHITELIST: '111, 222' } }, '111')
|
|
)
|
|
t.absent(
|
|
cmds.userAllowed({ env: { DISCORD_ID_WHITELIST: '111, 222' } }, '999')
|
|
)
|
|
t.absent(cmds.userAllowed({ env: { DISCORD_ID_WHITELIST: '111' } }, ''))
|
|
t.is(
|
|
cmds.whitelistCount({
|
|
env: { DISCORD_ID_WHITELIST: '342128351638585344' },
|
|
vfs: { env: {} }
|
|
}),
|
|
1,
|
|
'whitelist on ctx.env is visible when vfs.env exists but is empty'
|
|
)
|
|
t.ok(
|
|
cmds.userAllowed(
|
|
{
|
|
env: { DISCORD_ID_WHITELIST: '342128351638585344' },
|
|
vfs: { env: {} }
|
|
},
|
|
'342128351638585344',
|
|
{
|
|
authorizingIntegrationOwners: { 1: '342128351638585344' },
|
|
context: 1
|
|
}
|
|
),
|
|
'snowflake whitelist on ctx.env allows user-install'
|
|
)
|
|
t.absent(
|
|
cmds.userAllowed(
|
|
{
|
|
env: { DISCORD_ID_WHITELIST: '342128351638585344' },
|
|
vfs: { env: {} }
|
|
},
|
|
'999',
|
|
{ context: 1 }
|
|
)
|
|
)
|
|
const userIx = {
|
|
authorizingIntegrationOwners: { 1: '999' },
|
|
context: 2
|
|
}
|
|
t.absent(
|
|
cmds.userAllowed({ env: {} }, '999', userIx),
|
|
'empty whitelist denies user-install / DM'
|
|
)
|
|
t.absent(
|
|
cmds.userAllowed({ env: { DISCORD_ID_WHITELIST: '' } }, '999', {
|
|
context: 1
|
|
})
|
|
)
|
|
t.ok(
|
|
cmds.userAllowed(
|
|
{ env: { DISCORD_ID_WHITELIST: '111, 222' } },
|
|
'111',
|
|
userIx
|
|
)
|
|
)
|
|
t.absent(
|
|
cmds.userAllowed(
|
|
{ env: { DISCORD_ID_WHITELIST: '111, 222' } },
|
|
'999',
|
|
userIx
|
|
)
|
|
)
|
|
t.ok(cmds.userInstallEnabled({ env: {} }))
|
|
t.absent(cmds.userInstallEnabled({ env: { DISCORD_USER_INSTALL: '0' } }))
|
|
t.ok(cmds.isUserInstallInteraction(userIx))
|
|
t.absent(cmds.isUserInstallInteraction({ context: 0 }))
|
|
|
|
const denied = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
commandName: 'ping',
|
|
user: { id: '999' },
|
|
reply: async (p) => {
|
|
denied.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(denied[0] && /Access denied/.test(denied[0].content))
|
|
t.absent('ephemeral' in denied[0])
|
|
t.is(denied[0].flags, cmds.FLAG_EPHEMERAL)
|
|
|
|
const allowed = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
commandName: 'ping',
|
|
user: { id: '111' },
|
|
reply: async (p) => {
|
|
allowed.push(p)
|
|
}
|
|
}
|
|
)
|
|
const allowedBlob = JSON.stringify(allowed[0] || {})
|
|
t.ok(/pong/i.test(allowedBlob), 'allowed ping mentions pong')
|
|
t.absent('ephemeral' in allowed[0])
|
|
t.absent(allowed[0].flags)
|
|
|
|
const ctxWhite = { env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} }
|
|
async function denyKind(kind, extra) {
|
|
const replies = []
|
|
const updates = []
|
|
const responds = []
|
|
const ix = {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => kind === 'auto',
|
|
isButton: () => kind === 'button',
|
|
isStringSelectMenu: () => kind === 'select',
|
|
isModalSubmit: () => kind === 'modal',
|
|
customId: kind === 'modal' ? 'say:modal' : 'nav:panel',
|
|
user: { id: '999' },
|
|
message: {
|
|
id: 'm-white',
|
|
interaction: { user: { id: '111' } }
|
|
},
|
|
options: { getFocused: () => ({ name: 'cmd', value: 'una' }) },
|
|
fields: { getTextInputValue: () => 'hi' },
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
},
|
|
update: async (p) => {
|
|
updates.push(p)
|
|
},
|
|
respond: async (c) => {
|
|
responds.push(c)
|
|
}
|
|
}
|
|
Object.assign(ix, extra || {})
|
|
await cmds.dispatchInteraction(ctxWhite, ix)
|
|
return { replies, updates, responds }
|
|
}
|
|
const btn = await denyKind('button')
|
|
t.ok(btn.replies[0] && /Access denied/.test(btn.replies[0].content), 'button deny is ephemeral reply')
|
|
t.is(btn.updates.length, 0, 'denied button does not update the original message')
|
|
t.is(btn.replies[0].flags, cmds.FLAG_EPHEMERAL)
|
|
const sel = await denyKind('select')
|
|
t.ok(sel.replies[0] && /Access denied/.test(sel.replies[0].content), 'select deny is ephemeral reply')
|
|
t.is(sel.updates.length, 0, 'denied select does not update the original message')
|
|
const modal = await denyKind('modal')
|
|
t.ok(modal.replies[0] && /Access denied/.test(modal.replies[0].content), 'modal deny is ephemeral reply')
|
|
t.is(modal.updates.length, 0, 'denied modal does not update the original message')
|
|
const auto = await denyKind('auto')
|
|
t.ok(Array.isArray(auto.responds[0]) && auto.responds[0].length === 0, 'denied autocomplete is empty')
|
|
t.is(auto.replies.length, 0)
|
|
|
|
const ownerReplies = []
|
|
const ownerUpdates = []
|
|
await cmds.dispatchInteraction(ctxWhite, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => true,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
customId: 'nav:panel',
|
|
user: { id: '111' },
|
|
message: {
|
|
id: 'm-owner',
|
|
interaction: { user: { id: '222' } }
|
|
},
|
|
reply: async (p) => {
|
|
ownerReplies.push(p)
|
|
},
|
|
update: async (p) => {
|
|
ownerUpdates.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
ownerReplies[0] && /another operator/.test(ownerReplies[0].content),
|
|
'whitelist user cannot drive another operator\'s controls'
|
|
)
|
|
t.is(ownerUpdates.length, 0, 'owner mismatch does not update the original message')
|
|
|
|
const userInstallDenied = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: {}, vfs: {}, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
commandName: 'ping',
|
|
user: { id: '999' },
|
|
authorizingIntegrationOwners: { 1: '999' },
|
|
context: 2,
|
|
reply: async (p) => {
|
|
userInstallDenied.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(
|
|
userInstallDenied[0] && /Access denied/.test(userInstallDenied[0].content),
|
|
'user-install with empty whitelist is denied'
|
|
)
|
|
t.is(userInstallDenied[0].flags, cmds.FLAG_EPHEMERAL)
|
|
|
|
const userInstallAllowed = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
commandName: 'ping',
|
|
user: { id: '111' },
|
|
authorizingIntegrationOwners: { 1: '111' },
|
|
context: 1,
|
|
reply: async (p) => {
|
|
userInstallAllowed.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(
|
|
/pong/i.test(JSON.stringify(userInstallAllowed[0] || {})),
|
|
'whitelisted user can use profile-install commands'
|
|
)
|
|
})
|
|
|
|
test('user-install slash PUT is always global', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.is(
|
|
cmds.userInstallAuthorizeUrl('123'),
|
|
'https://discord.com/oauth2/authorize?client_id=123'
|
|
)
|
|
const cfg = cmds.userInstallAppConfig()
|
|
t.ok(cfg.integration_types_config['1'].oauth2_install_params.scopes.indexOf('applications.commands') >= 0)
|
|
t.ok(cfg.integration_types_config['0'].oauth2_install_params.scopes.indexOf('bot') >= 0)
|
|
const puts = []
|
|
const rest = {
|
|
put: async (route, opts) => {
|
|
puts.push({ route: route, body: opts && opts.body })
|
|
},
|
|
patch: async (route, opts) => {
|
|
puts.push({ route: route, patch: opts && opts.body })
|
|
}
|
|
}
|
|
const Routes = {
|
|
applicationCommands: (id) => 'global/' + id,
|
|
applicationGuildCommands: (id, g) => 'guild/' + id + '/' + g,
|
|
currentApplication: () => 'app/@me'
|
|
}
|
|
const body = [{ name: 'ping' }]
|
|
const put = await cmds.putSlashCommands(rest, Routes, 'app1', body, {
|
|
guildId: '99',
|
|
userInstall: true
|
|
})
|
|
t.ok(put.global)
|
|
t.ok(put.guild)
|
|
t.ok(puts.some((p) => p.route === 'global/app1'))
|
|
t.ok(puts.some((p) => p.route === 'guild/app1/99'))
|
|
await cmds.enableUserInstallApp(rest, Routes)
|
|
t.ok(puts.some((p) => p.route === 'app/@me' && p.patch && p.patch.integration_types_config))
|
|
})
|
|
|
|
test('Discord commands use unlocked identity, not guest env', (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const pk = Buffer.from('7fcd9053afcd', 'hex')
|
|
const ctx = {
|
|
env: { USER: 'guest', HOME: '/home/guest', LOGNAME: 'guest' },
|
|
vfs: { env: { USER: 'guest', HOME: '/home/guest', LOGNAME: 'guest' } },
|
|
identity: { state: 'unlocked', publicKey: pk },
|
|
b4a: { toString: (b, enc) => Buffer.from(b).toString(enc || 'utf8') }
|
|
}
|
|
t.is(cmds.sessionUser(ctx), '7fcd9053afcd')
|
|
t.is(ctx.vfs.env.USER, '7fcd9053afcd')
|
|
t.is(ctx.vfs.env.HOME, '/home/7fcd9053afcd')
|
|
t.is(cmds.sessionUser({ vfs: { env: { USER: 'alice', HOME: '/home/alice' } } }), 'alice')
|
|
})
|
|
|
|
test('slash autocomplete offers units and run shell commands', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const runChoices = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } }, console: {} },
|
|
{
|
|
isAutocomplete: () => true,
|
|
isChatInputCommand: () => false,
|
|
commandName: 'r',
|
|
options: { getFocused: () => ({ name: 'cmd', value: 'una' }) },
|
|
user: { id: '1' },
|
|
respond: async (c) => {
|
|
runChoices.push.apply(runChoices, c)
|
|
}
|
|
}
|
|
)
|
|
t.ok(
|
|
runChoices.some((c) => c.value === 'uname'),
|
|
'run autocomplete includes uname'
|
|
)
|
|
const cdChoices = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'alice', HOME: '/home/alice' }, vfs: { env: { USER: 'alice', HOME: '/home/alice' } }, console: {} },
|
|
{
|
|
isAutocomplete: () => true,
|
|
isChatInputCommand: () => false,
|
|
commandName: 'r',
|
|
options: { getFocused: () => ({ name: 'cmd', value: 'cd' }) },
|
|
user: { id: '1' },
|
|
respond: async (c) => {
|
|
cdChoices.push.apply(cdChoices, c)
|
|
}
|
|
}
|
|
)
|
|
t.ok(
|
|
cdChoices.some((c) => c.value === 'cd'),
|
|
'run autocomplete includes cd'
|
|
)
|
|
const unitChoices = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: {}, vfs: {}, console: {} },
|
|
{
|
|
isAutocomplete: () => true,
|
|
isChatInputCommand: () => false,
|
|
commandName: 'journal',
|
|
options: { getFocused: () => ({ name: 'unit', value: 'discord' }) },
|
|
user: { id: '1' },
|
|
respond: async (c) => {
|
|
unitChoices.push.apply(unitChoices, c)
|
|
}
|
|
}
|
|
)
|
|
t.ok(
|
|
unitChoices.some((c) => /discord/.test(c.value)),
|
|
'journal autocomplete includes bare-os-discord'
|
|
)
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
t.ok(body.some((c) => c.name === 'panel'))
|
|
t.ok(body.some((c) => c.name === 'edit'))
|
|
const runCmd = body.find((c) => c.name === 'r')
|
|
const runOpt = runCmd && (runCmd.options || []).find((o) => o.name === 'cmd')
|
|
t.ok(runOpt && runOpt.required, '/r cmd is required')
|
|
})
|
|
|
|
test('Discord /run is a persistent non-interactive shell', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const env = { USER: 'alice', HOME: '/home/alice', PWD: '/home/alice', HOSTNAME: 'bare-os' }
|
|
const ctx = {
|
|
env,
|
|
vfs: {
|
|
env,
|
|
chdir: async (p) => {
|
|
env.PWD = p
|
|
},
|
|
readdir: async (p) => {
|
|
if (p === '/bin') return ['ls', 'uname', 'cat']
|
|
if (p === '/tmp') return ['note.txt']
|
|
return []
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} },
|
|
exitCode: 0,
|
|
execLine: async (line) => {
|
|
const c = String(line || '').trim()
|
|
if (c === 'cd /tmp') {
|
|
env.PWD = '/tmp'
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (c === 'pwd') {
|
|
ctx.console.log(env.PWD)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (c === 'echo hi && echo there') {
|
|
ctx.console.log('hi')
|
|
ctx.console.log('there')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.console.log('ran:' + c)
|
|
ctx.exitCode = 0
|
|
}
|
|
}
|
|
const views = []
|
|
const push = async (p) => {
|
|
views.push(p)
|
|
}
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'sh1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: push
|
|
})
|
|
t.ok(/Shell/.test(JSON.stringify(views[0] || {})), 'server-side empty cmd still shows HUD')
|
|
t.absent(
|
|
/modal|Allowlisted/.test(JSON.stringify(views[0] || {})),
|
|
'no allowlist modal'
|
|
)
|
|
const runBody = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
const runDef = runBody.find((c) => c.name === 'r')
|
|
const cmdOpt = runDef && (runDef.options || []).find((o) => o.name === 'cmd')
|
|
t.ok(cmdOpt && cmdOpt.required, 'Discord prompts for /r cmd')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'sh1' },
|
|
options: { getSubcommand: () => '', getString: (k) => (k === 'cmd' ? 'cd /tmp' : '') },
|
|
reply: push
|
|
})
|
|
t.is(env.PWD, '/tmp', 'cd updates working directory')
|
|
t.ok(/tmp/.test(JSON.stringify(views[1] || {})), 'HUD shows new cwd')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'sh1' },
|
|
options: { getSubcommand: () => '', getString: (k) => (k === 'cmd' ? 'echo hi && echo there' : '') },
|
|
reply: push
|
|
})
|
|
t.ok(/hi/.test(JSON.stringify(views[2] || {})) && /there/.test(JSON.stringify(views[2] || {})), 'pipes/and-and allowed')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'sh1' },
|
|
options: { getSubcommand: () => '', getString: (k) => (k === 'cmd' ? 'exit' : '') },
|
|
reply: push
|
|
})
|
|
t.ok(/Stayed attached/.test(JSON.stringify(views[3] || {})), 'exit does not tear down the bot')
|
|
})
|
|
|
|
test('Discord /upload wget-saves an attachment into the /run cwd', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const env = { USER: 'alice', HOME: '/home/alice', PWD: '/home/alice' }
|
|
const written = {}
|
|
const lines = []
|
|
const ctx = {
|
|
env,
|
|
vfs: {
|
|
env,
|
|
chdir: async (p) => {
|
|
env.PWD = p
|
|
},
|
|
stat: async (p) => {
|
|
if (p === '/tmp' || p === '/home/alice') return { isDirectory: true, type: 'directory' }
|
|
if (written[p]) return { isFile: true, type: 'file', size: written[p].length }
|
|
throw new Error('ENOENT')
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
written[p] = Buffer.from(buf).toString('utf8')
|
|
}
|
|
},
|
|
console: { log() {}, error() {} },
|
|
exitCode: 0,
|
|
execLine: async (line) => {
|
|
lines.push(String(line))
|
|
const m = /wget[\s\S]*?-O\s+'([^']+)'\s+'([^']+)'/.exec(String(line))
|
|
if (m) {
|
|
written[m[1]] = 'cdn-body'
|
|
ctx.console.error(m[1])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|
|
|
|
const missing = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'upload',
|
|
user: { id: 'up1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: () => '',
|
|
getAttachment: () => null
|
|
},
|
|
reply: async (p) => {
|
|
missing.push(p)
|
|
}
|
|
})
|
|
t.ok(/Attach a file/.test(JSON.stringify(missing[0] || {})), 'requires an attachment')
|
|
|
|
const views = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'upload',
|
|
user: { id: 'up1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: () => '',
|
|
getAttachment: (k) =>
|
|
k === 'file'
|
|
? {
|
|
name: 'hello.txt',
|
|
url: 'https://cdn.discordapp.com/attachments/1/2/hello.txt',
|
|
size: 9,
|
|
contentType: 'text/plain'
|
|
}
|
|
: null
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/wget -T 60 -O /.test(lines[0] || ''), 'runs wget -O')
|
|
t.ok(/hello\.txt/.test(lines[0] || ''), 'uses attachment name')
|
|
t.ok(/cdn\.discordapp\.com/.test(lines[0] || ''), 'fetches Discord CDN url')
|
|
t.ok(/\/home\/alice\/hello\.txt/.test(lines[0] || ''), 'defaults to /run cwd')
|
|
t.ok(/Uploaded/.test(JSON.stringify(views[0] || {})), 'success embed')
|
|
t.ok(/hello\.txt/.test(JSON.stringify(views[0] || {})))
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'upload',
|
|
user: { id: 'up1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '/tmp/out.bin' : ''),
|
|
getAttachment: () => ({
|
|
name: 'hello.txt',
|
|
url: 'https://cdn.discordapp.com/attachments/1/2/hello.txt',
|
|
size: 9
|
|
})
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/\/tmp\/out\.bin/.test(lines[1] || ''), 'honors explicit file path')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'upload',
|
|
user: { id: 'up1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '/tmp/' : ''),
|
|
getAttachment: () => ({
|
|
name: 'hello.txt',
|
|
url: 'https://cdn.discordapp.com/attachments/1/2/hello.txt',
|
|
size: 9
|
|
})
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/\/tmp\/hello\.txt/.test(lines[2] || ''), 'directory path keeps the attachment name')
|
|
|
|
const denied = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'upload',
|
|
user: { id: 'up1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '/etc/passwd' : ''),
|
|
getAttachment: () => ({
|
|
name: 'p',
|
|
url: 'https://cdn.discordapp.com/attachments/1/2/p',
|
|
size: 1
|
|
})
|
|
},
|
|
reply: async (p) => {
|
|
denied.push(p)
|
|
}
|
|
})
|
|
t.ok(/not writable/.test(JSON.stringify(denied[0] || {})), 'refuses system paths')
|
|
t.is(lines.length, 3, 'denied upload does not run wget')
|
|
})
|
|
|
|
test('Discord /hdms manages extra Hyperdrives', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.alike(
|
|
cmds.parseHdmsList('notes\twritable\tz32abc\nwww\treadonly\tkey2\tephemeral\n'),
|
|
[
|
|
{ label: 'notes', mode: 'writable', key: 'z32abc', extra: '' },
|
|
{ label: 'www', mode: 'readonly', key: 'key2', extra: 'ephemeral' }
|
|
]
|
|
)
|
|
t.ok(cmds.pathOk('/mnt/notes'))
|
|
t.ok(cmds.pathOk('/mnt'))
|
|
t.ok(
|
|
cmds.pathWriteOk({ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } } }, '/mnt/notes/file.txt')
|
|
)
|
|
t.absent(
|
|
cmds.pathWriteOk({ env: { USER: 'alice' }, vfs: { env: { USER: 'alice' } } }, '/mnt')
|
|
)
|
|
|
|
const argvLog = []
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice', BARE_OS_IDENTITY: 'unlocked' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice', BARE_OS_IDENTITY: 'unlocked' },
|
|
readFile: async (p) => {
|
|
if (String(p).indexOf('hdms_health') >= 0) {
|
|
return Buffer.from(
|
|
JSON.stringify({
|
|
schema: 1,
|
|
active: true,
|
|
mountCount: 1,
|
|
labels: [{ label: 'notes', mode: 'writable' }],
|
|
note: 'No keys'
|
|
})
|
|
)
|
|
}
|
|
throw new Error('ENOENT')
|
|
}
|
|
},
|
|
identity: { state: 'unlocked' },
|
|
b4a: { toString: (b) => Buffer.from(b).toString('utf8') },
|
|
console: { log() {}, error() {} },
|
|
exitCode: 0,
|
|
runHdms: async (argv) => {
|
|
argvLog.push(argv.slice())
|
|
const sub = argv[1]
|
|
if (sub === 'list') {
|
|
ctx.console.log('notes\twritable\tz32abcde0123456789')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'create') {
|
|
ctx.console.log('Created ' + argv[2] + ' mounted=/mnt/' + argv[2])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'show') {
|
|
ctx.console.log(
|
|
JSON.stringify({
|
|
label: argv[2],
|
|
mode: 'writable',
|
|
key: 'z32abc',
|
|
writerSecretHex: 'deadbeefcafebabe'
|
|
})
|
|
)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'invite') {
|
|
ctx.console.log('invitez32token')
|
|
ctx.console.log('Invite includes HDMS drive')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'remove') {
|
|
ctx.console.log('Removed ' + argv[2])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|
|
|
|
const guest = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: { USER: 'guest', BARE_OS_IDENTITY: 'guest' }, vfs: { env: { USER: 'guest' } }, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hdms',
|
|
user: { id: 'g1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async (p) => {
|
|
guest.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(/unlocked|login/i.test(JSON.stringify(guest[0] || {})), 'guest is told to log in')
|
|
|
|
const views = []
|
|
const ix = {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hdms',
|
|
user: { id: 'h1' },
|
|
options: { getSubcommand: () => 'list', getString: () => '' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
}
|
|
await cmds.dispatchInteraction(ctx, ix)
|
|
const hud = JSON.stringify(views[0] || {})
|
|
t.ok(/notes/.test(hud), 'lists writable drive')
|
|
t.ok(collectCustomIds(views[0]).indexOf('hdms:create') >= 0, 'create button')
|
|
t.ok(collectCustomIds(views[0]).indexOf('hdms:pair') >= 0, 'pair button')
|
|
assertUniqueCustomIds(t, views[0], 'hdms hud')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hdms',
|
|
user: { id: 'h1' },
|
|
options: {
|
|
getSubcommand: () => 'create',
|
|
getString: (k) => (k === 'label' ? 'vault' : '')
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
argvLog.some((a) => a[1] === 'create' && a[2] === 'vault'),
|
|
'create calls runHdms'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hdms',
|
|
user: { id: 'h1' },
|
|
options: {
|
|
getSubcommand: () => 'show',
|
|
getString: (k) => (k === 'label' ? 'notes' : '')
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const shown = JSON.stringify(views[views.length - 1] || {})
|
|
t.ok(/notes/.test(shown))
|
|
t.absent(/deadbeefcafebabe/.test(shown), 'writer secret redacted')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hdms',
|
|
user: { id: 'h1' },
|
|
options: { getSubcommand: () => 'health', getString: () => '' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/health/i.test(JSON.stringify(views[views.length - 1] || {})))
|
|
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
const hdmsCmd = body.find((c) => c.name === 'hdms')
|
|
t.ok(hdmsCmd, 'slash /hdms exists')
|
|
const subs = (hdmsCmd.options || []).filter((o) => o.type === 1).map((o) => o.name)
|
|
t.ok(subs.indexOf('create') >= 0 && subs.indexOf('pair') >= 0 && subs.indexOf('invite') >= 0)
|
|
})
|
|
|
|
test('Discord /holesail manages tunnels and the bare-holesail unit', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.alike(
|
|
cmds.parseHolesailList(
|
|
[
|
|
'state: ~/.holesail/state.json',
|
|
'daemon: managed bare-holesail running',
|
|
'www\tserver\tenabled=true\tlive=true\tport=8088\thost=127.0.0.1\turl=hs://abc',
|
|
'peer\tclient\tenabled=false\tlive=false\tport=9000'
|
|
].join('\n')
|
|
),
|
|
{
|
|
rows: [
|
|
{
|
|
id: 'www',
|
|
mode: 'server',
|
|
enabled: true,
|
|
live: true,
|
|
port: '8088',
|
|
host: '127.0.0.1',
|
|
udp: '',
|
|
secure: '',
|
|
url: 'hs://abc',
|
|
invalid: false
|
|
},
|
|
{
|
|
id: 'peer',
|
|
mode: 'client',
|
|
enabled: false,
|
|
live: false,
|
|
port: '9000',
|
|
host: '',
|
|
udp: '',
|
|
secure: '',
|
|
url: '',
|
|
invalid: false
|
|
}
|
|
],
|
|
daemon: 'managed bare-holesail running',
|
|
path: '~/.holesail/state.json'
|
|
}
|
|
)
|
|
t.is(cmds.parseHolesailShow('id\twww\nmode\tserver\nurl\ths://abc\n').url, 'hs://abc')
|
|
|
|
const argvLog = []
|
|
const sysLog = []
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: { env: { USER: 'alice', HOME: '/home/alice' } },
|
|
identity: { state: 'unlocked' },
|
|
b4a: { toString: (b) => Buffer.from(b).toString('utf8') },
|
|
console: { log() {}, error() {} },
|
|
exitCode: 0,
|
|
bareOsRunHolesailCli: async (argv) => {
|
|
argvLog.push(argv.slice())
|
|
const sub = argv[1]
|
|
if (sub === 'list') {
|
|
ctx.console.log('state: ~/.holesail/state.json')
|
|
ctx.console.log('daemon: managed bare-holesail running')
|
|
ctx.console.log(
|
|
'www\tserver\tenabled=true\tlive=true\tport=8088\thost=127.0.0.1\turl=hs://shareme'
|
|
)
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'status') {
|
|
ctx.console.log('state: ~/.holesail/state.json')
|
|
ctx.console.log('daemon: managed bare-holesail running')
|
|
ctx.console.log('connections: 1\tenabled=1\tlive=1\tserver=1\tclient=0')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'show') {
|
|
ctx.console.log('id\t' + argv[2])
|
|
ctx.console.log('mode\tserver')
|
|
ctx.console.log('enabled\ttrue')
|
|
ctx.console.log('live\ttrue')
|
|
ctx.console.log('port\t8088')
|
|
ctx.console.log('seed\tyes')
|
|
ctx.console.log('url\ths://shareme')
|
|
ctx.console.log('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'add') {
|
|
ctx.console.log('wrote ~/.holesail/state.json')
|
|
ctx.console.log('started ' + argv[2])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'remove') {
|
|
ctx.console.log('removed ' + argv[2])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (sub === 'start' || sub === 'stop' || sub === 'restart' || sub === 'enable' || sub === 'disable') {
|
|
ctx.console.log(sub + ' ' + argv[2])
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.exitCode = 1
|
|
},
|
|
bareOsRunSystemctlCli: async (argv) => {
|
|
sysLog.push(argv.slice())
|
|
ctx.console.log('unit ' + argv.join(' '))
|
|
ctx.exitCode = 0
|
|
}
|
|
}
|
|
|
|
const missing = []
|
|
await cmds.dispatchInteraction(
|
|
{ env: {}, vfs: {}, console: {} },
|
|
{
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'g1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async (p) => {
|
|
missing.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(/bareOsRunHolesailCli|unavailable/i.test(JSON.stringify(missing[0] || {})))
|
|
|
|
const views = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'h1' },
|
|
options: { getSubcommand: () => 'list', getString: () => '' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const hud = JSON.stringify(views[0] || {})
|
|
t.ok(/www/.test(hud), 'lists server tunnel')
|
|
t.ok(collectCustomIds(views[0]).indexOf('holesail:addsrv') >= 0, 'add server button')
|
|
t.ok(collectCustomIds(views[0]).indexOf('holesail:addcli') >= 0, 'add client button')
|
|
t.ok(collectCustomIds(views[0]).indexOf('holesail:svcstart') >= 0, 'start unit button')
|
|
assertUniqueCustomIds(t, views[0], 'holesail hud')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'h1' },
|
|
options: {
|
|
getSubcommand: () => 'add',
|
|
getString: (k) => {
|
|
if (k === 'id') return 'lab'
|
|
if (k === 'mode') return 'server'
|
|
if (k === 'port') return '9090'
|
|
return ''
|
|
}
|
|
},
|
|
deferReply: async () => {},
|
|
deferred: false,
|
|
replied: false,
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
},
|
|
editReply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
argvLog.some((a) => a[1] === 'add' && a[2] === 'lab' && a.indexOf('--server') >= 0),
|
|
'add calls holesail CLI'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'h1' },
|
|
options: {
|
|
getSubcommand: () => 'show',
|
|
getString: (k) => (k === 'id' ? 'www' : '')
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const shown = JSON.stringify(views[views.length - 1] || {})
|
|
t.ok(/www/.test(shown))
|
|
t.ok(/hs:\/\/shareme/.test(shown))
|
|
t.absent(/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.test(shown), 'seed hex redacted')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'h1' },
|
|
options: { getSubcommand: () => 'status', getString: () => '' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/status/i.test(JSON.stringify(views[views.length - 1] || {})))
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'holesail',
|
|
user: { id: 'h1' },
|
|
options: {
|
|
getSubcommand: () => 'service',
|
|
getString: (k) => (k === 'action' ? 'restart' : '')
|
|
},
|
|
deferReply: async () => {},
|
|
deferred: false,
|
|
replied: false,
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
},
|
|
editReply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
sysLog.some((a) => a[0] === 'systemctl' && a[1] === 'restart' && a[2] === 'bare-holesail'),
|
|
'service restart uses systemctl'
|
|
)
|
|
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
const hsCmd = body.find((c) => c.name === 'holesail')
|
|
t.ok(hsCmd, 'slash /holesail exists')
|
|
const hsSubs = (hsCmd.options || []).filter((o) => o.type === 1).map((o) => o.name)
|
|
t.ok(hsSubs.indexOf('add') >= 0 && hsSubs.indexOf('edit') >= 0 && hsSubs.indexOf('service') >= 0)
|
|
})
|
|
|
|
test('Discord /agent is gated on config and runs the guest agent', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
const agentCmd = body.find((c) => c.name === 'agent')
|
|
t.ok(agentCmd, 'slash /agent exists')
|
|
const subs = (agentCmd.options || []).filter((o) => o.type === 1).map((o) => o.name)
|
|
t.ok(subs.indexOf('ask') >= 0 && subs.indexOf('reset') >= 0 && subs.indexOf('status') >= 0)
|
|
for (const name of [
|
|
'skills',
|
|
'todos',
|
|
'plan',
|
|
'hooks',
|
|
'history',
|
|
'recap',
|
|
'undo',
|
|
'rewind',
|
|
'compact',
|
|
'export',
|
|
'remember',
|
|
'models',
|
|
'config',
|
|
'stop'
|
|
]) {
|
|
t.ok(subs.indexOf(name) >= 0, '/agent has ' + name)
|
|
}
|
|
const askSub = (agentCmd.options || []).find((o) => o.type === 1 && o.name === 'ask')
|
|
const askPrompt = ((askSub && askSub.options) || []).find((o) => o.name === 'prompt')
|
|
t.ok(askPrompt && askPrompt.required, '/agent ask prompt is required')
|
|
t.ok(askPrompt && askPrompt.min_length >= 1, '/agent ask prompt has min length')
|
|
t.ok(
|
|
((askSub && askSub.options) || []).some((o) => o.name === 'new' && o.type === 5),
|
|
'/agent ask has optional new flag'
|
|
)
|
|
t.ok(
|
|
((askSub && askSub.options) || []).some((o) => o.name === 'auto' && o.type === 5),
|
|
'/agent ask has optional auto flag'
|
|
)
|
|
t.ok(
|
|
((askSub && askSub.options) || []).some((o) => o.name === 'compact' && o.type === 5),
|
|
'/agent ask has optional compact flag'
|
|
)
|
|
t.ok(
|
|
((askSub && askSub.options) || []).some((o) => o.name === 'max_turns' && o.type === 4),
|
|
'/agent ask has max_turns'
|
|
)
|
|
const rememberSub = (agentCmd.options || []).find((o) => o.type === 1 && o.name === 'remember')
|
|
t.ok(
|
|
((rememberSub && rememberSub.options) || []).some((o) => o.name === 'text' && o.required),
|
|
'/agent remember text is required'
|
|
)
|
|
t.ok(
|
|
cmds.settingsSpecs.some((s) => s.jsonKey === 'emergency_stop_mutations'),
|
|
'settings expose emergency_stop_mutations'
|
|
)
|
|
t.ok(
|
|
cmds.settingsSpecs.some((s) => s.jsonKey === 'todo_nudge_enabled'),
|
|
'settings expose todo_nudge_enabled'
|
|
)
|
|
|
|
const views = []
|
|
const tree = {}
|
|
const lines = []
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
mkdir: async (p) => {
|
|
if (!Array.isArray(tree[p])) tree[p] = []
|
|
},
|
|
readFile: async (p) => {
|
|
if (typeof tree[p] !== 'string') {
|
|
const e = new Error('ENOENT')
|
|
e.code = 'ENOENT'
|
|
throw e
|
|
}
|
|
return Buffer.from(tree[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
tree[p] = Buffer.from(buf).toString('utf8')
|
|
}
|
|
},
|
|
console: {},
|
|
execLine: async (line, opts) => {
|
|
lines.push({ line: String(line), timeoutMs: opts && opts.timeoutMs })
|
|
t.is(ctx.env.BARE_OS_AGENT_DISCORD, '1', 'discord markdown mode')
|
|
t.absent(ctx.env.BARE_OS_AGENT_PLAIN, 'does not force TTY-plain replies')
|
|
ctx.console.log('→ read_file\n**hello from** `' + String(line) + '`')
|
|
ctx.exitCode = 0
|
|
},
|
|
bareOsQvacAvailable: () => false
|
|
}
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'list', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const empty = JSON.stringify(views[0] || {})
|
|
t.ok(/not configured|agent --setup/i.test(empty), 'unconfigured HUD explains setup')
|
|
t.absent(
|
|
(views[0].components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:ask')
|
|
),
|
|
'Ask hidden until configured'
|
|
)
|
|
|
|
tree['~/.agent/config.json'] =
|
|
'{"backend":"qvac","qvac_model":"QWEN3_1_7B_INST_Q4","rest_api_key":"sk-secret"}\n'
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'status', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/QVAC host bridge is unavailable/i.test(JSON.stringify(views[1] || {})))
|
|
|
|
ctx.bareOsQvacAvailable = () => true
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'list', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const ready = views[2]
|
|
t.ok(/QWEN3_1_7B_INST_Q4/.test(JSON.stringify(ready || {})))
|
|
t.ok(
|
|
(ready.components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:ask')
|
|
),
|
|
'Ask shown when QVAC is ready'
|
|
)
|
|
t.ok(
|
|
(ready.components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:models')
|
|
),
|
|
'Models button on HUD'
|
|
)
|
|
t.ok(
|
|
(ready.components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:inspect')
|
|
),
|
|
'Inspect menu on HUD'
|
|
)
|
|
t.ok(
|
|
(ready.components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:recap')
|
|
),
|
|
'Recap button on HUD'
|
|
)
|
|
t.ok(cmds.qvacChatModels.some((m) => m.id === 'QWEN3_8B_INST_Q4_K_M'))
|
|
const parsed = cmds.parseOpenAiModels({
|
|
data: [{ id: 'grok-4' }, { id: 'text-embedding-3-small' }]
|
|
})
|
|
t.is(parsed.length, 1)
|
|
t.is(parsed[0].id, 'grok-4')
|
|
await cmds.applyAgentModel(ctx, 'QWEN3_8B_INST_Q4_K_M')
|
|
t.ok(JSON.parse(tree['~/.agent/config.json']).qvac_model === 'QWEN3_8B_INST_Q4_K_M')
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'models', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const picker = views[views.length - 1]
|
|
t.ok(/QVAC models/.test(JSON.stringify(picker || {})))
|
|
t.ok(
|
|
(picker.components || []).some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'agent:mdl')
|
|
),
|
|
'model select present'
|
|
)
|
|
|
|
tree['~/.agent/config.json'] =
|
|
'{"backend":"rest","provider":"groq","rest_api_key":"gsk","rest_base_url":"https://api.groq.com/openai/v1","model":"old"}\n'
|
|
ctx.httpFetch = async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
data: [{ id: 'llama-3.3-70b-versatile' }, { id: 'llama-3.1-8b-instant' }]
|
|
})
|
|
})
|
|
const live = await cmds.fetchRestModels(ctx, JSON.parse(tree['~/.agent/config.json']))
|
|
t.is(live.source, 'live')
|
|
t.ok(live.models.some((m) => m.id === 'llama-3.3-70b-versatile'))
|
|
await cmds.applyAgentModel(ctx, 'llama-3.1-8b-instant')
|
|
t.is(JSON.parse(tree['~/.agent/config.json']).model, 'llama-3.1-8b-instant')
|
|
t.absent(JSON.parse(tree['~/.agent/config.json']).qvac_model)
|
|
tree['~/.agent/config.json'] =
|
|
'{"backend":"rest","provider":"groq","rest_api_key":"sk-secret","rest_base_url":"https://api.groq.com/openai/v1","model":"llama-3.3-70b-versatile"}\n'
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'config', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const cfgBlob = JSON.stringify(views[views.length - 1] || {})
|
|
t.ok(/\[set\]/.test(cfgBlob), 'API key shown as set')
|
|
t.absent(/sk-secret/.test(cfgBlob), 'API key value never sent to Discord')
|
|
tree['~/.agent/config.json'] =
|
|
'{"backend":"qvac","qvac_model":"QWEN3_1_7B_INST_Q4"}\n'
|
|
|
|
tree['~/.agent/history.json'] = '[{"role":"user","content":"hi"}]\n'
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'reset', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.is(tree['~/.agent/history.json'], '[]\n')
|
|
|
|
const emptyAsk = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: {
|
|
getSubcommand: () => 'ask',
|
|
getString: () => ' ',
|
|
getBoolean: () => null
|
|
},
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
emptyAsk.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
emptyAsk[0] && /Prompt is required/i.test(JSON.stringify(emptyAsk[0])),
|
|
'empty /agent ask is rejected'
|
|
)
|
|
t.absent(
|
|
lines.some((l) => /agent /.test(l.line)),
|
|
'empty ask does not exec agent'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: {
|
|
getSubcommand: () => 'ask',
|
|
getString: (k) => (k === 'prompt' ? 'list /bin' : k === 'model' ? 'QWEN3_4B_INST_Q4_K_M' : ''),
|
|
getBoolean: (k) =>
|
|
k === 'new' || k === 'plan' || k === 'auto' || k === 'compact' ? true : null,
|
|
getInteger: (k) => (k === 'max_turns' ? 12 : null)
|
|
},
|
|
user: { id: 'ag1' },
|
|
deferReply: async () => {
|
|
views.push({ deferred: true })
|
|
},
|
|
editReply: async (p) => {
|
|
views.push(p)
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(lines.some((l) => /agent /.test(l.line) && /list \/bin/.test(l.line)))
|
|
t.ok(
|
|
lines.some(
|
|
(l) =>
|
|
/--new/.test(l.line) &&
|
|
/--plan/.test(l.line) &&
|
|
/--auto/.test(l.line) &&
|
|
/--compact/.test(l.line) &&
|
|
/--max-turns 12/.test(l.line) &&
|
|
/--model/.test(l.line) &&
|
|
/list \/bin/.test(l.line)
|
|
),
|
|
'new + plan + auto + compact + max-turns + model are passed through to /bin/agent'
|
|
)
|
|
t.ok(lines.some((l) => l.timeoutMs > 60000), 'agent timeout longer than /r')
|
|
t.ok(
|
|
views.some((v) => /hello from/.test(JSON.stringify(v || {}))),
|
|
'agent stdout captured into Discord'
|
|
)
|
|
t.ok(
|
|
views.some((v) => {
|
|
const d = v && v.embeds && v.embeds[0] && v.embeds[0].description
|
|
return d && /\*\*hello\*\*/.test(d) && !/```[\s\S]*\*\*hello\*\*/.test(d)
|
|
}) ||
|
|
views.some((v) => /Discord markdown/.test(JSON.stringify(v || {}))),
|
|
'agent reply is Discord markdown, not a code fence'
|
|
)
|
|
t.absent(ctx.env.BARE_OS_AGENT_PLAIN, 'plain env restored')
|
|
t.absent(ctx.env.BARE_OS_AGENT_DISCORD, 'discord env restored')
|
|
|
|
const parsedProg = cmds.parseAgentProgress(
|
|
'2026-08-18T00:00:00.000Z read_file /tmp/a\n2026-08-18T00:00:01.000Z iteration 2 tools read_file,run_command\n'
|
|
)
|
|
t.is(parsedProg.steps[0].tool, 'read_file')
|
|
t.is(parsedProg.iteration, 2)
|
|
const split = cmds.splitAgentStdout(
|
|
'→ read_file\n[process] iteration 1 tools read_file\n**Ready**\n- /bin/ls\n'
|
|
)
|
|
t.is(split.answer, '**Ready**\n- /bin/ls')
|
|
t.ok(cmds.formatAgentProcess(parsedProg).indexOf('`read_file`') >= 0)
|
|
t.ok(cmds.markdownPages('# hi\n\n```\ncode\n', 80)[0].indexOf('```') >= 0)
|
|
|
|
const denied = []
|
|
await cmds.dispatchInteraction(
|
|
{
|
|
env: { DISCORD_ID_WHITELIST: '111' },
|
|
vfs: { env: { DISCORD_ID_WHITELIST: '111' } },
|
|
console: {}
|
|
},
|
|
{
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
user: { id: '999' },
|
|
options: { getSubcommand: () => 'ask', getString: () => 'pwn' },
|
|
reply: async (p) => {
|
|
denied.push(p)
|
|
}
|
|
}
|
|
)
|
|
t.ok(denied[0] && /Access denied/.test(denied[0].content), 'whitelist still gates /agent')
|
|
|
|
const inspectLines = []
|
|
ctx.execLine = async (line) => {
|
|
inspectLines.push(String(line))
|
|
ctx.console.log('## skills\n| id | name |\n| --- | --- |\n| demo | Demo |')
|
|
ctx.exitCode = 0
|
|
}
|
|
const inspectViews = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: { getSubcommand: () => 'skills', getString: () => '' },
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
inspectViews.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
inspectLines.some((l) => /^agent skills$/.test(l)),
|
|
'skills inspect calls guest /bin/agent'
|
|
)
|
|
t.ok(/demo/.test(JSON.stringify(inspectViews[0] || {})), 'skills inspect shows output')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: {
|
|
getSubcommand: () => 'remember',
|
|
getString: (k) => (k === 'text' ? 'holesail keys live in ~/.holesail' : '')
|
|
},
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
inspectViews.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
inspectLines.some((l) => /agent remember/.test(l) && /holesail keys/.test(l)),
|
|
'remember inspect quotes the fact'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
commandName: 'agent',
|
|
options: {
|
|
getSubcommand: () => 'rewind',
|
|
getString: () => '',
|
|
getInteger: (k) => (k === 'steps' ? 2 : null)
|
|
},
|
|
user: { id: 'ag1' },
|
|
reply: async (p) => {
|
|
inspectViews.push(p)
|
|
}
|
|
})
|
|
t.ok(
|
|
inspectLines.some((l) => /agent rewind/.test(l) && /['"]?2['"]?/.test(l)),
|
|
'rewind passes steps'
|
|
)
|
|
|
|
tree['~/.agent/config.json'] =
|
|
'{"backend":"rest","provider":"groq","rest_api_key":"gsk","model":"x"}\n'
|
|
const restNoUrl = cmds.agentReady(ctx, JSON.parse(tree['~/.agent/config.json']))
|
|
t.absent(restNoUrl.ok, 'REST without base URL is not ready')
|
|
})
|
|
|
|
test('Discord /run captures stdout, raw writes, and writeScreen (no TTY leak)', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const leaked = []
|
|
const origOut = process.stdout.write
|
|
const origErr = process.stderr.write
|
|
process.stdout.write = function (chunk, enc, cb) {
|
|
leaked.push('out:' + String(chunk))
|
|
return origOut.apply(process.stdout, arguments)
|
|
}
|
|
process.stderr.write = function (chunk, enc, cb) {
|
|
leaked.push('err:' + String(chunk))
|
|
return origErr.apply(process.stderr, arguments)
|
|
}
|
|
const views = []
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice', PWD: '/home/alice' },
|
|
vfs: { env: { USER: 'alice', HOME: '/home/alice', PWD: '/home/alice' } },
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: {
|
|
log() {
|
|
leaked.push('console.log')
|
|
},
|
|
error() {
|
|
leaked.push('console.error')
|
|
}
|
|
},
|
|
writeScreen(s) {
|
|
leaked.push('writeScreen:' + String(s))
|
|
},
|
|
exitCode: 0,
|
|
execLine: async (line) => {
|
|
const c = String(line || '').trim()
|
|
if (c === 'echo hi') {
|
|
// Same path as /bin/echo: bareOsEmitRaw → process.stdout.write
|
|
process.stdout.write('hi\n')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (c === 'cat note') {
|
|
ctx.bareOsBinWrite(Buffer.from('file-body\n'))
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
if (c === 'mixed') {
|
|
ctx.console.log('from-console')
|
|
ctx.console.error('from-err')
|
|
process.stdout.write('from-stdout\n')
|
|
process.stderr.write('from-stderr\n')
|
|
ctx.writeScreen('from-screen\n')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.console.log('ran:' + c)
|
|
ctx.exitCode = 0
|
|
}
|
|
}
|
|
try {
|
|
const captured = await cmds.captureOutput(ctx, async () => {
|
|
process.stdout.write('raw-out\n')
|
|
ctx.console.log('via-log')
|
|
})
|
|
t.ok(/raw-out/.test(captured) && /via-log/.test(captured), 'captureOutput joins console + stdout')
|
|
t.absent(
|
|
leaked.some((l) => /raw-out|via-log/.test(l)),
|
|
'captureOutput does not leak to the host TTY'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'cap1' },
|
|
options: { getSubcommand: () => '', getString: (k) => (k === 'cmd' ? 'echo hi' : '') },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/hi/.test(JSON.stringify(views[0] || {})), '/run echo appears in Discord')
|
|
t.absent(
|
|
leaked.some((l) => l === 'out:hi\n' || l === 'out:hi'),
|
|
'/run echo does not print on the booter TTY'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'cap1' },
|
|
options: { getSubcommand: () => '', getString: (k) => (k === 'cmd' ? 'mixed' : '') },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const mixed = JSON.stringify(views[1] || {})
|
|
t.ok(/from-console/.test(mixed), 'console.log in Discord')
|
|
t.ok(/from-err/.test(mixed), 'console.error in Discord')
|
|
t.ok(/from-stdout/.test(mixed), 'process.stdout in Discord')
|
|
t.ok(/from-stderr/.test(mixed), 'process.stderr in Discord')
|
|
t.ok(/from-screen/.test(mixed), 'writeScreen in Discord')
|
|
t.absent(
|
|
leaked.some((l) => /from-console|from-err|from-stdout|from-stderr|from-screen/.test(l)),
|
|
'mixed /run output stays off the booter TTY'
|
|
)
|
|
|
|
ctx.execLine = async (line) => {
|
|
if (String(line).trim() === 'lscolor') {
|
|
t.is(ctx.env.NO_COLOR, '1', 'NO_COLOR during /run')
|
|
t.is(ctx.bareOsStdoutCaptured, true, 'stdout marked captured')
|
|
process.stdout.write('\u001b[01;32magent\u001b[0m \u001b[01;32mappctl\u001b[0m\n')
|
|
ctx.exitCode = 0
|
|
return
|
|
}
|
|
ctx.exitCode = 0
|
|
}
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'r',
|
|
user: { id: 'cap1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'cmd' ? 'lscolor' : '')
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const colorBlob = JSON.stringify(views[views.length - 1] || {})
|
|
t.ok(/agent/.test(colorBlob) && /appctl/.test(colorBlob), 'ls names survive')
|
|
t.absent(/01;32m|\[0m|\u001b/.test(colorBlob), 'ANSI color codes are not sent to Discord')
|
|
t.absent(ctx.env.NO_COLOR, 'NO_COLOR restored after /run')
|
|
} finally {
|
|
process.stdout.write = origOut
|
|
process.stderr.write = origErr
|
|
}
|
|
})
|
|
|
|
test('Discord strips ANSI color from every outbound payload', (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const colored = '\u001b[01;32magent\u001b[0m \u001b[01;34mappctl\u001b[0m'
|
|
t.is(cmds.stripAnsi(colored), 'agent appctl')
|
|
t.is(cmds.stripAnsi('\u001b]8;;https://x\u0007link\u001b]8;;\u0007'), 'link')
|
|
const packed = cmds.packEmbed({
|
|
title: '\u001b[1mShell\u001b[0m',
|
|
desc: '```\n' + colored + '\n```',
|
|
fields: [{ name: 'Listing', value: colored }],
|
|
footer: '\u001b[2mfooter\u001b[0m'
|
|
})
|
|
const blob = JSON.stringify(packed.embed)
|
|
t.ok(/agent/.test(blob) && /appctl/.test(blob))
|
|
t.absent(/01;32m|01;34m|\[0m|\u001b/.test(blob), 'embed has no CSI')
|
|
const payload = cmds.replyPayload({ text: colored })
|
|
t.ok(/agent/.test(payload.content) && /appctl/.test(payload.content))
|
|
t.absent(/01;32m|\u001b/.test(payload.content), 'content has no CSI')
|
|
})
|
|
|
|
test('Discord plugins load JSON and JS from ~/.discord/plugins', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const tree = {
|
|
'~/.discord': ['plugins'],
|
|
'~/.discord/plugins': ['hello.json', 'echo.js', 'run.json', 'disabled.txt'],
|
|
'~/.discord/plugins/hello.json': JSON.stringify({
|
|
name: 'hello',
|
|
description: 'Say hi',
|
|
options: [{ name: 'who', description: 'Name', required: false }],
|
|
run: 'echo hello-${who:-world}'
|
|
}),
|
|
'~/.discord/plugins/echo.js':
|
|
'function register(bot) {\n' +
|
|
' bot.command({\n' +
|
|
' name: "echo",\n' +
|
|
' description: "Echo",\n' +
|
|
' options: [{ name: "text", description: "Text", required: true }],\n' +
|
|
' run: async function (ev) {\n' +
|
|
' return { title: "echo", desc: ev.opt("text") }\n' +
|
|
' }\n' +
|
|
' })\n' +
|
|
'}\n',
|
|
'~/.discord/plugins/run.json': JSON.stringify({ name: 'run', description: 'should be rejected' }),
|
|
'~/.discord/plugins/disabled.txt': ''
|
|
}
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
mkdir: async (p) => {
|
|
if (!tree[p]) tree[p] = []
|
|
},
|
|
readdir: async (p) => {
|
|
if (!Array.isArray(tree[p])) throw new Error('ENOTDIR')
|
|
return tree[p].slice()
|
|
},
|
|
readFile: async (p) => {
|
|
if (typeof tree[p] !== 'string') throw new Error('ENOENT')
|
|
return Buffer.from(tree[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
tree[p] = Buffer.from(buf).toString('utf8')
|
|
},
|
|
stat: async (p) => {
|
|
if (Array.isArray(tree[p])) return { isDirectory: true }
|
|
if (typeof tree[p] === 'string') return { isFile: true }
|
|
throw new Error('ENOENT')
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} },
|
|
execLine: async (line) => {
|
|
ctx.console.log('out:' + line)
|
|
}
|
|
}
|
|
const loaded = await cmds.loadPlugins(ctx)
|
|
t.ok(loaded.some((p) => p.name === 'hello' && p.ok))
|
|
t.ok(loaded.some((p) => p.name === 'echo' && p.ok))
|
|
t.ok(loaded.some((p) => p.name === 'run' && !p.ok), 'reserved /run rejected')
|
|
|
|
const body = await Promise.resolve(
|
|
cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() }, ctx)
|
|
)
|
|
t.ok(body.some((c) => c.name === 'hello'))
|
|
t.ok(body.some((c) => c.name === 'echo'))
|
|
t.ok(body.some((c) => c.name === 'plugins'))
|
|
|
|
const replies = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'hello',
|
|
user: { id: 'pl1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'who' ? 'raven' : '')
|
|
},
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
}
|
|
})
|
|
t.ok(/hello-raven/.test(JSON.stringify(replies[0] || {})), 'JSON plugin expands ${who}')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'echo',
|
|
user: { id: 'pl1' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'text' ? 'ping' : '')
|
|
},
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
}
|
|
})
|
|
t.ok(/ping/.test(JSON.stringify(replies[1] || {})), 'JS plugin run()')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'plugins',
|
|
user: { id: 'pl1' },
|
|
options: { getSubcommand: () => 'list', getString: () => '' },
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
}
|
|
})
|
|
t.ok(/Plugins/.test(JSON.stringify(replies[2] || {})))
|
|
})
|
|
|
|
test('Discord modal editor writes allowed paths and blocks secrets', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const ctxBase = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: { env: { USER: 'alice', HOME: '/home/alice' } }
|
|
}
|
|
t.is(cmds.pathWriteOk(ctxBase, '~/notes.txt'), '~/notes.txt')
|
|
t.is(cmds.pathWriteOk(ctxBase, '/tmp/x.txt'), '/tmp/x.txt')
|
|
t.is(cmds.pathWriteOk(ctxBase, '/home/alice/todo.md'), '/home/alice/todo.md')
|
|
t.absent(cmds.pathWriteOk(ctxBase, '/etc/os-release'))
|
|
t.absent(cmds.pathWriteOk(ctxBase, '~/.discord/.env'))
|
|
t.absent(cmds.pathWriteOk(ctxBase, '/proc/uptime'))
|
|
const split = cmds.editChunks('abcdefghij')
|
|
t.is(split.chunks.join(''), 'abcdefghij')
|
|
t.absent(split.truncated)
|
|
const big = cmds.editChunks(Array(20001 + 1).join('x'))
|
|
t.ok(big.truncated)
|
|
t.is(big.chunks.join('').length, 20000)
|
|
const payload = cmds.editModalPayload('~/notes.txt', ['hello'])
|
|
t.is(payload.custom_id, 'edit:save')
|
|
t.is(payload.components.length, 1)
|
|
t.is(payload.components[0].components[0].value, 'hello')
|
|
|
|
const written = []
|
|
const files = { '~/notes.txt': 'old' }
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
readFile: async (p) => {
|
|
if (files[p] == null) {
|
|
const e = new Error('ENOENT')
|
|
e.code = 'ENOENT'
|
|
throw e
|
|
}
|
|
return Buffer.from(files[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
written.push({ p: p, body: Buffer.from(buf).toString('utf8') })
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} }
|
|
}
|
|
const shown = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'edit',
|
|
user: { id: '42' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '~/notes.txt' : '')
|
|
},
|
|
showModal: async (m) => {
|
|
shown.push(m)
|
|
}
|
|
})
|
|
t.ok(shown[0] && shown[0].custom_id === 'edit:save')
|
|
t.ok(shown[0].components[0].components[0].value === 'old')
|
|
|
|
const replies = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => true,
|
|
customId: 'edit:save',
|
|
user: { id: '42' },
|
|
fields: {
|
|
getTextInputValue: (id) => (id === 'c0' ? 'new body' : '')
|
|
},
|
|
reply: async (p) => {
|
|
replies.push(p)
|
|
}
|
|
})
|
|
t.is(written[0].p, '~/notes.txt')
|
|
t.is(written[0].body, 'new body')
|
|
t.ok(replies[0] && replies[0].embeds && /Saved/.test(replies[0].embeds[0].title))
|
|
})
|
|
|
|
test('Discord /create picks a path and writes a new file, refusing overwrite', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const written = []
|
|
const files = { '~/notes.txt': 'old' }
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
stat: async (p) => {
|
|
if (files[p] == null) {
|
|
const e = new Error('ENOENT')
|
|
e.code = 'ENOENT'
|
|
throw e
|
|
}
|
|
return { isFile: true, size: files[p].length }
|
|
},
|
|
readFile: async (p) => {
|
|
if (files[p] == null) {
|
|
const e = new Error('ENOENT')
|
|
e.code = 'ENOENT'
|
|
throw e
|
|
}
|
|
return Buffer.from(files[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
written.push({ p: p, body: Buffer.from(buf).toString('utf8') })
|
|
files[p] = Buffer.from(buf).toString('utf8')
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} }
|
|
}
|
|
|
|
const picker = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'create',
|
|
user: { id: '99' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async (p) => {
|
|
picker.push(p)
|
|
}
|
|
})
|
|
t.ok(picker[0] && picker[0].embeds && /Create a file/.test(picker[0].embeds[0].title))
|
|
t.ok(
|
|
picker[0].components &&
|
|
picker[0].components.some((row) =>
|
|
(row.components || []).some((c) => c.custom_id === 'create:pick')
|
|
),
|
|
'path select is present'
|
|
)
|
|
|
|
const exists = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'create',
|
|
user: { id: '99' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '~/notes.txt' : '')
|
|
},
|
|
reply: async (p) => {
|
|
exists.push(p)
|
|
}
|
|
})
|
|
t.ok(exists[0] && exists[0].embeds && /Already exists/.test(exists[0].embeds[0].title))
|
|
t.is(written.length, 0)
|
|
|
|
const shown = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'create',
|
|
user: { id: '77' },
|
|
options: {
|
|
getSubcommand: () => '',
|
|
getString: (k) => (k === 'path' ? '~/fresh.txt' : '')
|
|
},
|
|
showModal: async (m) => {
|
|
shown.push(m)
|
|
}
|
|
})
|
|
t.is(shown[0] && shown[0].custom_id, 'create:save')
|
|
t.ok(shown[0].title && /Create/.test(shown[0].title))
|
|
|
|
const created = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => true,
|
|
customId: 'create:save',
|
|
user: { id: '77' },
|
|
fields: {
|
|
getTextInputValue: (id) => (id === 'c0' ? 'hello world' : '')
|
|
},
|
|
reply: async (p) => {
|
|
created.push(p)
|
|
}
|
|
})
|
|
t.is(written[0].p, '~/fresh.txt')
|
|
t.is(written[0].body, 'hello world')
|
|
t.ok(created[0] && created[0].embeds && /Created/.test(created[0].embeds[0].title))
|
|
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
t.ok(body.some((c) => c.name === 'create'))
|
|
})
|
|
|
|
test('Discord /files browser lists, opens dirs, mkdir and delete', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.is(cmds.joinPath('~', 'a'), '~/a')
|
|
t.is(cmds.parentPath('~/a/b'), '~/a')
|
|
t.is(cmds.parentPath('~'), '~')
|
|
|
|
const tree = {
|
|
'~': ['notes.txt', 'docs'],
|
|
'~/docs': ['readme.md'],
|
|
'~/notes.txt': 'hi',
|
|
'~/docs/readme.md': 'doc'
|
|
}
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice' },
|
|
readdir: async (p) => {
|
|
const n = tree[p]
|
|
if (!n || !Array.isArray(n)) {
|
|
const e = new Error('ENOTDIR')
|
|
throw e
|
|
}
|
|
return n.slice()
|
|
},
|
|
stat: async (p) => {
|
|
if (Array.isArray(tree[p])) return { isDirectory: true, type: 'directory' }
|
|
if (typeof tree[p] === 'string') {
|
|
return { isFile: true, type: 'file', size: tree[p].length }
|
|
}
|
|
const e = new Error('ENOENT')
|
|
throw e
|
|
},
|
|
lstat: async (p) => ctx.vfs.stat(p),
|
|
readFile: async (p) => {
|
|
if (typeof tree[p] !== 'string') throw new Error('ENOENT')
|
|
return Buffer.from(tree[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
tree[p] = Buffer.from(buf).toString('utf8')
|
|
const parent = cmds.parentPath(p)
|
|
const base = p.slice(parent.length).replace(/^\//, '')
|
|
if (Array.isArray(tree[parent]) && tree[parent].indexOf(base) < 0) {
|
|
tree[parent].push(base)
|
|
}
|
|
},
|
|
mkdir: async (p) => {
|
|
tree[p] = tree[p] || []
|
|
const parent = cmds.parentPath(p)
|
|
const base = p.slice(parent.length).replace(/^\//, '')
|
|
if (Array.isArray(tree[parent]) && tree[parent].indexOf(base) < 0) {
|
|
tree[parent].push(base)
|
|
}
|
|
},
|
|
unlink: async (p) => {
|
|
delete tree[p]
|
|
const parent = cmds.parentPath(p)
|
|
const base = p.slice(parent.length).replace(/^\//, '')
|
|
if (Array.isArray(tree[parent])) {
|
|
tree[parent] = tree[parent].filter((n) => n !== base)
|
|
}
|
|
},
|
|
rm: async (p) => ctx.vfs.unlink(p)
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
console: { log() {}, error() {} }
|
|
}
|
|
|
|
const views = []
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'files',
|
|
user: { id: 'fm1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
const first = JSON.stringify(views[0] || {})
|
|
t.ok(/notes\.txt/.test(first) && /docs/.test(first), 'lists home entries')
|
|
t.ok(views[0].components.some((row) => (row.components || []).some((c) => c.custom_id === 'fm:pick')))
|
|
assertUniqueCustomIds(t, views[0], '/files')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => true,
|
|
isModalSubmit: () => false,
|
|
customId: 'fm:pick',
|
|
values: ['docs'],
|
|
user: { id: 'fm1' },
|
|
update: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(/readme\.md/.test(JSON.stringify(views[1] || {})), 'opened docs folder')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => true,
|
|
customId: 'fm:mkdir',
|
|
user: { id: 'fm1' },
|
|
fields: { getTextInputValue: () => 'more' },
|
|
update: async (p) => {
|
|
views.push(p)
|
|
},
|
|
reply: async (p) => {
|
|
views.push(p)
|
|
}
|
|
})
|
|
t.ok(Array.isArray(tree['~/docs/more']), 'mkdir created folder')
|
|
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
t.ok(body.some((c) => c.name === 'files'))
|
|
})
|
|
|
|
test('Discord /settings browses groups, toggles live flags, and never writes secrets', async (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.ok(cmds.settingsSpecs.some((s) => s.id === 'theme'))
|
|
t.ok(cmds.settingsSpecs.some((s) => s.id === 'errexit'))
|
|
const secretKey = /^(DISCORD_TOKEN|rest_api_key|saslPass|require_confirm_token)$/
|
|
t.ok(cmds.settingsSpecs.every((s) => !secretKey.test(s.env || '') && !secretKey.test(s.jsonKey || '')))
|
|
|
|
const tree = {
|
|
'~': ['.barerc'],
|
|
'~/.barerc': '# bare\nexport BARE_OS_COMPACT_MENU=0\n',
|
|
'~/.discord/.env': 'DISCORD_TOKEN=super-secret\nDISCORD_GUILD_ID=1\n',
|
|
'~/.agent/config.json':
|
|
'{"backend":"rest","provider":"groq","rest_api_key":"sk-secret","rest_base_url":"https://api.groq.com/openai/v1","model":"old"}\n',
|
|
'~/.irc/config.json': '{"nick":"os","autojoin":["#bare"]}\n'
|
|
}
|
|
const ctx = {
|
|
env: { USER: 'alice', HOME: '/home/alice', BARE_OS_COMPACT_MENU: '0' },
|
|
vfs: {
|
|
env: { USER: 'alice', HOME: '/home/alice', BARE_OS_COMPACT_MENU: '0' },
|
|
mkdir: async (p) => {
|
|
if (!Array.isArray(tree[p])) tree[p] = []
|
|
},
|
|
readFile: async (p) => {
|
|
if (typeof tree[p] !== 'string') throw new Error('ENOENT')
|
|
return Buffer.from(tree[p])
|
|
},
|
|
writeFile: async (p, buf) => {
|
|
tree[p] = Buffer.from(buf).toString('utf8')
|
|
}
|
|
},
|
|
b4a: {
|
|
toString: (b) => Buffer.from(b).toString('utf8'),
|
|
from: (s) => Buffer.from(s)
|
|
},
|
|
shellAliases: { ll: 'ls -la' },
|
|
bareOsListThemes: () => ['default', 'nord', 'dracula'],
|
|
bareOsApplyTheme: async () => {
|
|
ctx.themeApplied = (ctx.themeApplied || 0) + 1
|
|
},
|
|
console: { log() {}, error() {} }
|
|
}
|
|
|
|
const views = []
|
|
const push = async (p) => {
|
|
views.push(p)
|
|
}
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => true,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => false,
|
|
commandName: 'settings',
|
|
user: { id: 'set1' },
|
|
options: { getSubcommand: () => '', getString: () => '' },
|
|
reply: push
|
|
})
|
|
const first = JSON.stringify(views[0] || {})
|
|
t.ok(/Settings/.test(first), 'opens settings')
|
|
t.ok(/Theme/.test(first), 'shows appearance knobs')
|
|
t.absent(/super-secret|sk-secret|DISCORD_TOKEN/.test(first), 'no secrets in view')
|
|
t.ok(views[0].components.some((row) => (row.components || []).some((c) => c.custom_id === 'set:group')))
|
|
assertUniqueCustomIds(t, views[0], '/settings')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => true,
|
|
isModalSubmit: () => false,
|
|
customId: 'set:group',
|
|
values: ['shell'],
|
|
user: { id: 'set1' },
|
|
update: push
|
|
})
|
|
t.ok(/Compact completion/.test(JSON.stringify(views[1] || {})), 'shell group')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => true,
|
|
isModalSubmit: () => false,
|
|
customId: 'set:item',
|
|
values: ['compact'],
|
|
user: { id: 'set1' },
|
|
update: push
|
|
})
|
|
t.ok(
|
|
views[2] &&
|
|
views[2].components &&
|
|
views[2].components.some((row) => (row.components || []).some((c) => c.custom_id === 'set:val')),
|
|
'opens value picker'
|
|
)
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => true,
|
|
isModalSubmit: () => false,
|
|
customId: 'set:val',
|
|
values: ['1'],
|
|
user: { id: 'set1' },
|
|
update: push
|
|
})
|
|
t.is(ctx.vfs.env.BARE_OS_COMPACT_MENU, '1')
|
|
t.ok(/export BARE_OS_COMPACT_MENU=1/.test(tree['~/.barerc']), 'persists compact to barerc')
|
|
|
|
const compact = cmds.settingsSpecs.find((s) => s.id === 'compact')
|
|
await cmds.settingsApply(ctx, compact, '0')
|
|
t.is(ctx.vfs.env.BARE_OS_COMPACT_MENU, '0')
|
|
|
|
const theme = cmds.settingsSpecs.find((s) => s.id === 'theme')
|
|
await cmds.settingsApply(ctx, theme, 'nord')
|
|
t.is(ctx.vfs.env.BARE_OS_THEME, 'nord')
|
|
t.ok(/theme nord/.test(tree['~/.barerc']))
|
|
t.ok(ctx.themeApplied >= 1)
|
|
|
|
const model = cmds.settingsSpecs.find((s) => s.id === 'ag_model')
|
|
await cmds.settingsApply(ctx, model, 'QWEN3_NEW')
|
|
const agent = JSON.parse(tree['~/.agent/config.json'])
|
|
t.is(agent.model, 'QWEN3_NEW')
|
|
t.is(agent.rest_api_key, 'sk-secret', 'does not strip existing api key from json')
|
|
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => true,
|
|
isModalSubmit: () => false,
|
|
customId: 'set:group',
|
|
values: ['aliases'],
|
|
user: { id: 'set1' },
|
|
update: push
|
|
})
|
|
await cmds.dispatchInteraction(ctx, {
|
|
isChatInputCommand: () => false,
|
|
isAutocomplete: () => false,
|
|
isButton: () => false,
|
|
isStringSelectMenu: () => false,
|
|
isModalSubmit: () => true,
|
|
customId: 'set:alias',
|
|
user: { id: 'set1' },
|
|
fields: { getTextInputValue: () => 'gst=git status' },
|
|
update: push,
|
|
reply: push
|
|
})
|
|
t.is(ctx.shellAliases.gst, 'git status')
|
|
t.ok(/alias gst=/.test(tree['~/.barerc']) && /git status/.test(tree['~/.barerc']))
|
|
|
|
const white = cmds.settingsSpecs.find((s) => s.id === 'whitelist')
|
|
await cmds.settingsApply(ctx, white, '111,222')
|
|
t.is(ctx.vfs.env.DISCORD_ID_WHITELIST, '111,222')
|
|
t.ok(/DISCORD_ID_WHITELIST=111,222/.test(tree['~/.discord/.env']))
|
|
t.ok(/DISCORD_TOKEN=super-secret/.test(tree['~/.discord/.env']), 'keeps existing token')
|
|
const userInstall = cmds.settingsSpecs.find((s) => s.id === 'user_install')
|
|
t.ok(userInstall && userInstall.env === 'DISCORD_USER_INSTALL')
|
|
await cmds.settingsApply(ctx, userInstall, '1')
|
|
t.is(ctx.vfs.env.DISCORD_USER_INSTALL, '1')
|
|
t.ok(/DISCORD_USER_INSTALL=1/.test(tree['~/.discord/.env']))
|
|
|
|
let denied = null
|
|
try {
|
|
await cmds.settingsApply(ctx, {
|
|
id: 'tok',
|
|
kind: 'string',
|
|
env: 'DISCORD_TOKEN',
|
|
persist: 'discord'
|
|
}, 'leak')
|
|
} catch (err) {
|
|
denied = err
|
|
}
|
|
t.ok(denied, 'refuses to write DISCORD_TOKEN')
|
|
t.ok(/DISCORD_TOKEN=super-secret/.test(tree['~/.discord/.env']))
|
|
|
|
const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() })
|
|
t.ok(body.some((c) => c.name === 'settings'))
|
|
})
|
|
|
|
test('proc JSON and systemctl text are parsed into embed fields', (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
t.is(cmds.prettyBytes(1536), '1.5 KiB')
|
|
t.is(cmds.parseMeminfo('MemTotal: 2048 kB\nMemAvailable: 1024 kB\n').MemTotal, 2048 * 1024)
|
|
const units = cmds.parseSystemctlList(
|
|
'UNIT LOAD PRESET ACTIVE SUB DESCRIPTION\nbare-os-www static enabled active running www\nbare-os-chat static enabled inactive dead chat\n'
|
|
)
|
|
t.is(units[0].unit, 'bare-os-www')
|
|
t.is(units[0].active, 'active')
|
|
const lim = cmds.formatRlimits({
|
|
rlimits: {
|
|
RLIMIT_NOFILE: { cur: 1024, max: 4096 },
|
|
RLIMIT_NPROC: { cur: 64, max: 256 }
|
|
}
|
|
})
|
|
t.ok(lim.some((f) => f.name === 'Open files' && /1024/.test(f.value)))
|
|
const feat = cmds.formatFeatures({ features: { tuiSdk: true, ircTls: false } })
|
|
t.ok(feat.some((f) => f.name === 'On' && /tuiSdk/.test(f.value)))
|
|
const doc = cmds.formatDoctor({
|
|
peerAdmission: { allowlistConfigured: false, denylistConfigured: false, requireCapsConfigured: false },
|
|
hostnameMutation: { enabled: false },
|
|
mfaExtensionPoints: ['delegated-signer']
|
|
})
|
|
t.ok(doc.fields.some((f) => f.name === 'Peer allowlist' && f.value === 'open'))
|
|
t.ok(doc.fields.some((f) => f.name === 'Hostname set' && f.value === 'locked'))
|
|
t.absent(doc.fields.some((f) => /schema|note|evaluator|api/i.test(f.name)))
|
|
const swarm = cmds.formatSwarm({
|
|
peerCount: 3,
|
|
protocol: 'bare-os-v1',
|
|
topicHex: 'abcd'.repeat(16)
|
|
})
|
|
t.ok(swarm.some((f) => f.name === 'Peers' && f.value === '3'))
|
|
const net = cmds.formatNetSummary({
|
|
schemaVersion: 2,
|
|
topicHex: '9863e4407570aabbccddeeff00112233',
|
|
peerCount: 1,
|
|
seedHandshakeError: null,
|
|
seedRole: 'seeder',
|
|
replicationQueueDepth: null,
|
|
peerFirewallAcceptedTotal: null,
|
|
peerFirewallRejectedTotal: null,
|
|
peerFirewallInboundTotal: null,
|
|
peerFirewallOutboundTotal: null,
|
|
replicationQueue: {
|
|
role: 'seeder',
|
|
protocol: 'bare-os-v1',
|
|
manifestPathCount: 448,
|
|
localRamBlockCount: 1,
|
|
hypercoreLengthHint: 12362,
|
|
queueDepthEstimate: 448,
|
|
snapshotWorkflowNote: 'Align long replication with corestore-snapshot-style cutover.',
|
|
atMs: 1770000000000
|
|
},
|
|
stagingSlot: {
|
|
role: 'seeder',
|
|
protocol: 'bare-os-v1',
|
|
activeSlot: 'a',
|
|
pendingSlot: null,
|
|
previousSlot: null,
|
|
canarySlot: null,
|
|
drainDeadlineMs: null
|
|
}
|
|
})
|
|
t.ok(net.fields.some((f) => f.name === 'Peers' && f.value === '1'))
|
|
t.ok(net.fields.some((f) => f.name === 'Role' && f.value === 'seeder'))
|
|
t.ok(net.fields.some((f) => f.name === 'Topic' && /9863e4407570aabb/.test(f.value)))
|
|
t.ok(net.fields.some((f) => f.name === 'Core length' && f.value === '12362'))
|
|
t.ok(net.fields.some((f) => f.name === 'Active slot' && f.value === 'a'))
|
|
t.absent(net.fields.some((f) => /peerFirewall|schemaVersion|Handshake|Drain/.test(f.name)))
|
|
t.absent(net.fields.some((f) => f.value === '—'))
|
|
t.ok(/1.*peer/.test(net.desc) && /seeder/.test(net.desc))
|
|
const df = cmds.formatHostDf({
|
|
host: { platform: 'darwin', arch: 'arm64', totalmem: 8 * 1024 * 1024 * 1024, freemem: 2 * 1024 * 1024 * 1024 },
|
|
peers: 1,
|
|
statvfs: { volume_class: 'personal', f_blocks: 1000, f_bavail: 400, f_frsize: 4096 }
|
|
})
|
|
t.ok(df.some((f) => f.name === 'RAM'))
|
|
t.ok(df.some((f) => f.name === 'Volume' && f.value === 'personal'))
|
|
const snap = cmds.prettySnapshot('Host', {
|
|
schema: 1,
|
|
note: 'ok',
|
|
platform: 'darwin',
|
|
nested: { a: 1, b: true }
|
|
})
|
|
t.ok(snap.embeds && snap.embeds[0].fields && snap.embeds[0].fields.length >= 1)
|
|
t.ok(
|
|
snap.embeds[0].fields.every((f) => String(f.value).charAt(0) !== '{'),
|
|
'fields are not raw JSON objects'
|
|
)
|
|
t.ok(snap.embeds[0].fields.some((f) => f.name === 'Platform' && f.value === 'darwin'))
|
|
t.ok(snap.embeds[0].fields.some((f) => f.name === 'Nested' && /\*\*A\*\*/.test(f.value)))
|
|
t.absent(snap.embeds[0].fields.some((f) => f.name === 'schema' || f.value === '—'))
|
|
})
|
|
|
|
test('process.emitWarning polyfill and reply payload avoid Bare throw', (t) => {
|
|
const req = createRequire(fileURLToPath(import.meta.url))
|
|
const cmds = req('./lib/bare-os-discord-commands-guest.cjs')
|
|
const stub = {}
|
|
cmds.installProcessEmitWarning(stub)
|
|
t.is(typeof stub.emitWarning, 'function')
|
|
stub.emitWarning('Supplying "ephemeral" is deprecated')
|
|
const open = cmds.replyPayload({ text: 'pong' })
|
|
t.ok(!('ephemeral' in open))
|
|
const hidden = cmds.replyPayload({ text: 'nope', ephemeral: true })
|
|
t.ok(!('ephemeral' in hidden))
|
|
t.ok(!('fetchReply' in hidden))
|
|
t.ok(!('fetchReply' in open))
|
|
t.is(hidden.flags, 64)
|
|
const ws = req('./vendor/bare-discord-js/src/adapters/whatwg-ws.cjs')
|
|
t.is(typeof ws.installBareOsProcessEmitWarning, 'function')
|
|
})
|