import test from 'brittle' import { BARE_OS_DISCORD_SERVICE_ENV, BARE_OS_DISCORD_UNIT, applyDiscordExtrasToSession, bareOsDiscordInitdEnabled, hydrateBareOsDiscordSessionEnv, readBareOsDiscordServiceEnv, syncBareOsDiscordInitd, stopBareOsDiscordInitd } from './lib/services/bare-os-discord-initd.js' import { findBareServiceDefinition, listBareServices, unregisterBareService } from './lib/initd/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/services/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(names.indexOf('status') >= 0, 'slash /status exists') 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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/services/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/.test(JSON.stringify(v || {}))), 'agent stdout captured into Discord' ) const finalAsk = [...views].reverse().find((v) => { const e = v && v.embeds && v.embeds[0] return e && e.description && e.title !== 'Agent · working' }) const finalDesc = (finalAsk && finalAsk.embeds[0].description) || '' t.ok(/\*\*hello\*\*/.test(finalDesc), 'final agent reply includes Discord markdown') t.ok(!/```[\s\S]*\*\*hello\*\*/.test(finalDesc), 'final reply is not wrapped in a code fence') t.ok(!/\*\*qvac\*\*/i.test(finalDesc), 'no backend preamble on final reply') t.ok(!/_list \/bin_/.test(finalDesc), 'no italic prompt echo on final reply') t.ok(!/Loading model/i.test(finalDesc), 'no model-load chatter on final reply') t.ok( !/^\*\*[A-Za-z0-9_-]+\*\* · `/.test(finalDesc), 'final reply does not start with backend · model · time' ) 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( 'Loading model…\n[qvac] ensuring model QWEN3_1_7B_INST_Q4 (ctx=8192)\n→ read_file\n[process] iteration 1 tools read_file\nDone: listed /bin\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/services/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/services/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/services/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/services/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/services/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/services/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/services/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'])) const presType = cmds.settingsSpecs.find((s) => s.id === 'pres_type') t.ok(presType && presType.env === 'DISCORD_ACTIVITY_TYPE') const seenPresence = [] ctx.bareOsDiscordClient = { user: { setPresence: (p) => { seenPresence.push(p) } } } await cmds.settingsApply(ctx, presType, 'watching') t.is(ctx.vfs.env.DISCORD_ACTIVITY_TYPE, 'watching') t.ok(seenPresence.length, 'settings apply presence live') t.ok(seenPresence[0].activities && seenPresence[0].activities[0].type === 3) 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/services/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/services/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') }) test('Discord DMs are whitelist-only agent chat; /status updates presence', async (t) => { const req = createRequire(fileURLToPath(import.meta.url)) const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs') t.ok(typeof cmds.dispatchMessage === 'function') t.ok(cmds.isDirectMessage({ channel: { type: 1 }, channelId: 'c' })) t.absent(cmds.isDirectMessage({ guildId: 'g', channel: { type: 0 } })) const def = cmds.presenceFromEnv({ env: {} }) t.is(def.status, 'online') t.is(def.typeName, 'playing') t.is(def.name, 'Bare OS') const watch = cmds.presenceFromEnv({ env: { DISCORD_STATUS: 'idle', DISCORD_ACTIVITY_TYPE: 'watching', DISCORD_ACTIVITY_NAME: 'the swarm' } }) t.is(watch.status, 'idle') t.is(watch.type, 3) t.is(watch.name, 'the swarm') const off = cmds.presenceFromEnv({ env: { DISCORD_ACTIVITY_NAME: 'off' } }) t.is(off.name, '') const seen = [] const client = { user: { setPresence: (p) => { seen.push(p) } } } t.ok( cmds.applyPresence( { env: { DISCORD_ACTIVITY_TYPE: 'listening', DISCORD_ACTIVITY_NAME: 'kernel logs', DISCORD_STATUS: 'dnd' } }, client ) ) t.is(seen[0].status, 'dnd') t.is(seen[0].activities[0].type, 2) t.is(seen[0].activities[0].name, 'kernel logs') const body = cmds.buildSlashCommands({ SlashCommandBuilder: mockSlash() }) const statusCmd = body.find((c) => c.name === 'status') t.ok(statusCmd, 'slash /status registered') function makeDm(opts) { const posted = [] const msg = { author: { id: opts.uid, bot: !!opts.bot }, content: opts.content || '', guildId: opts.guildId || null, guild: opts.guild || null, channelId: 'dm1', channel: { type: opts.guildId ? 0 : 1, isDMBased: () => !opts.guildId, sendTyping: async () => { posted.push({ typing: true }) }, send: async (p) => { posted.push(p) return { id: 'm2', edit: async (q) => { posted.push(q) return q } } } }, reply: async (p) => { posted.push(p) return { id: 'm1', edit: async (q) => { posted.push(q) return q } } } } return { msg: msg, posted: posted } } const bot = makeDm({ uid: '111', bot: true, content: 'hello' }) t.absent(await cmds.dispatchMessage({ env: { DISCORD_ID_WHITELIST: '111' } }, bot.msg)) const emptyWl = makeDm({ uid: '111', content: 'hello' }) t.ok(await cmds.dispatchMessage({ env: {} }, emptyWl.msg), 'empty whitelist denies DM') t.ok( emptyWl.posted.some((p) => /Access denied/.test(typeof p === 'string' ? p : JSON.stringify(p))), 'empty whitelist DM gets deny text' ) const stranger = makeDm({ uid: '999', content: 'hello' }) t.ok( await cmds.dispatchMessage({ env: { DISCORD_ID_WHITELIST: '111' } }, stranger.msg) ) t.ok( stranger.posted.some((p) => /Access denied/.test(typeof p === 'string' ? p : JSON.stringify(p))) ) const helpDm = makeDm({ uid: '111', content: 'help' }) t.ok( await cmds.dispatchMessage({ env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} }, helpDm.msg) ) t.ok( helpDm.posted.some((p) => /Direct messages/i.test(JSON.stringify(p || {}))), 'whitelisted DM help' ) const lines = [] const tree = { '~/.agent/config.json': '{"backend":"rest","provider":"groq","rest_api_key":"gsk","rest_base_url":"https://api.groq.com/openai/v1","model":"llama"}\n' } const agentDm = makeDm({ uid: '111', content: 'list /bin' }) const ctx = { env: { DISCORD_ID_WHITELIST: '111', USER: 'alice', HOME: '/home/alice' }, vfs: { env: { DISCORD_ID_WHITELIST: '111', 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) => { lines.push(line) ctx.console.log('**hello** from DM') ctx.exitCode = 0 }, bareOsQvacAvailable: () => false } t.ok(await cmds.dispatchMessage(ctx, agentDm.msg)) t.ok( lines.some((l) => /agent /.test(String(l)) && /list \/bin/.test(String(l))), 'whitelisted DM runs guest agent' ) t.ok( agentDm.posted.some((p) => /\*\*hello\*\*/.test(JSON.stringify(p || {}))), 'DM reply is the agent answer' ) const guildPing = makeDm({ uid: '111', content: 'ping', guildId: 'g1', guild: { id: 'g1' } }) t.ok(await cmds.dispatchMessage({ env: { DISCORD_ID_WHITELIST: '111' } }, guildPing.msg)) t.ok(guildPing.posted.some((p) => p === 'pong'), 'guild ping still pongs') const guildChat = makeDm({ uid: '111', content: 'please hack the gibson', guildId: 'g1', guild: { id: 'g1' } }) t.absent( await cmds.dispatchMessage({ env: { DISCORD_ID_WHITELIST: '111' } }, guildChat.msg), 'guild text is not an agent chat' ) const views = [] await cmds.dispatchInteraction( { env: { DISCORD_ID_WHITELIST: '111', DISCORD_ACTIVITY_TYPE: 'playing', DISCORD_ACTIVITY_NAME: 'Bare OS' }, vfs: { env: {}, mkdir: async () => {}, writeFile: async () => {}, readFile: async () => { const e = new Error('ENOENT') e.code = 'ENOENT' throw e } }, console: {}, bareOsDiscordClient: { user: { setPresence: (p) => { views.push(p) } } } }, { isChatInputCommand: () => true, isAutocomplete: () => false, isButton: () => false, isStringSelectMenu: () => false, commandName: 'status', user: { id: '111' }, options: { getSubcommand: () => '', getString: (k) => k === 'activity' ? 'watching' : k === 'name' ? 'the swarm' : k === 'state' ? 'idle' : '' }, reply: async (p) => { views.push(p) } } ) t.ok( views.some((v) => v && v.activities && v.activities[0] && v.activities[0].type === 3), '/status applies watching presence' ) t.ok( views.some((v) => v && v.embeds && /watching/i.test(JSON.stringify(v))), '/status embed confirms activity' ) }) test('Discord DM channel lets the agent reach out (whitelist only)', async (t) => { const req = createRequire(fileURLToPath(import.meta.url)) const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs') t.ok(typeof cmds.sendChannelMessage === 'function') t.ok(typeof cmds.bindChannelHook === 'function') const tree = {} const sent = [] const ctx = { env: { DISCORD_ID_WHITELIST: '111' }, vfs: { env: { DISCORD_ID_WHITELIST: '111' }, mkdir: async () => {}, 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: {}, bareOsDiscordClient: { users: { fetch: async (id) => ({ send: async (payload) => { sent.push({ id: id, payload: payload }) return { id: 'm' } } }) } } } const denied = await cmds.sendChannelMessage(ctx, { text: 'hi', userId: '999' }) t.absent(denied.ok, 'cannot DM off-whitelist') t.is(denied.error, 'whitelist_denied') t.is(sent.length, 0) const noPartner = await cmds.sendChannelMessage( { env: { DISCORD_ID_WHITELIST: '111,222' }, vfs: { env: { DISCORD_ID_WHITELIST: '111,222' }, mkdir: ctx.vfs.mkdir, readFile: ctx.vfs.readFile, writeFile: ctx.vfs.writeFile }, console: {} }, { text: 'hello' } ) t.absent(noPartner.ok, 'no partner until someone DMs') t.is(noPartner.error, 'no_channel_partner') await cmds.channelRemember(ctx, '111', 'hey bot') const rec = JSON.parse(tree['~/.discord/channel.json']) t.is(rec.userId, '111') t.ok(rec.open) const inbox = JSON.parse(tree['~/.discord/inbox.json']) t.ok(inbox.some((m) => m.text === 'hey bot')) const ok = await cmds.sendChannelMessage(ctx, { text: 'I can reach you' }) t.ok(ok.ok) t.absent(ok.queued) t.is(sent.length, 1) t.is(sent[0].id, '111') t.is(sent[0].payload.content, 'I can reach you') const queuedCtx = { env: { DISCORD_ID_WHITELIST: '111' }, vfs: ctx.vfs, console: {} } const queued = await cmds.sendChannelMessage(queuedCtx, { text: 'queued while offline', userId: '111' }) t.ok(queued.ok && queued.queued) const outbox = JSON.parse(tree['~/.discord/outbox.json']) t.ok(outbox.some((m) => m.text === 'queued while offline')) const drain = await cmds.drainChannelOutbox(ctx) t.ok(drain.ok) t.ok(drain.drained >= 1) t.ok(sent.some((s) => s.payload && s.payload.content === 'queued while offline')) cmds.bindChannelHook(ctx) t.ok(typeof ctx.bareOsDiscordSendDm === 'function') const viaHook = await ctx.bareOsDiscordSendDm({ text: 'via hook' }) t.ok(viaHook.ok) t.ok(sent.some((s) => s.payload && s.payload.content === 'via hook')) const st = await cmds.channelStatus(ctx) t.ok(st.open) t.is(st.userId, '111') t.ok(st.liveClient) })