2616 lines
83 KiB
JavaScript
2616 lines
83 KiB
JavaScript
import {
|
|
test,
|
|
b4a,
|
|
Hyperdrive,
|
|
Corestore,
|
|
mkdirSync,
|
|
rmSync,
|
|
readFile,
|
|
readdir,
|
|
path,
|
|
fileURLToPath,
|
|
PassThrough,
|
|
XTERM_CLEAR_SCROLLBACK_AND_VIEWPORT,
|
|
runKernelFromSource,
|
|
runBinCommand,
|
|
createGitFsFromVfs,
|
|
runGitCli,
|
|
createStreamLineReader,
|
|
sanitizeInteractiveShellLine,
|
|
createVfs,
|
|
BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE,
|
|
parseHdmsPairArgv,
|
|
hcCrypto,
|
|
idEnc,
|
|
decodeRwShareOffer,
|
|
decodeShareOffer,
|
|
HDMS_AUTOPASS_SHARE_KEY,
|
|
HDMS_AUTOPASS_SHARE_RW_KEY,
|
|
normalizeWriterSecretHex,
|
|
buildBareOsHypercorePackHrpcLifecycleProcJson,
|
|
buildBareOsPearCorestoreHrpcProcJson,
|
|
buildBareOsPearInspectLoggerTlsProcJson,
|
|
buildBareOsBareRuntimeProtoMuxProcJson,
|
|
buildBareOsBootGraphProcJson,
|
|
pathnameExpandShellWord,
|
|
createBareOsIpc,
|
|
BareOsSwarmConnectionManager,
|
|
createBareOsLifecycleStateMachine,
|
|
runTransactionalLifecycleHooks,
|
|
bareOsRegisterCorestoreSuspendResumeHooks,
|
|
bareOsInstallCorestoreSuspendResumeHooks,
|
|
bareOsFindOrphanProcessRows,
|
|
topologicalOrderKernelExtensions,
|
|
resolveKernelExtensionsFull,
|
|
createBareOsProtomuxAliasRegistry,
|
|
bareOsHttpUrlAllowed,
|
|
bareOsParseBareAclText,
|
|
bareOsAclDeniesSubject,
|
|
raceWithAbortAndTimeout,
|
|
tokenize,
|
|
expandWord,
|
|
execShellLine,
|
|
expandArgvAliases,
|
|
defaultShellAliases,
|
|
loadBarerc,
|
|
BARERC_SKELETON,
|
|
splitTokensBySemicolon,
|
|
splitTokensByAndOr,
|
|
bareOsShellAstSnapshot,
|
|
planShellRedirections,
|
|
buildShellExecutionGraph,
|
|
dispatchShellTrapSignal,
|
|
BARE_OS_EXIT_STATUS_ENV,
|
|
syncBareOsExitStatusEnv,
|
|
getBareOsPipelineLimits,
|
|
DEFAULT_PIPELINE_MAX_STAGES,
|
|
tokenizeBareShellLineDetailed,
|
|
decodeBareOsDollarQuote,
|
|
applyBareOsThemeFromEnv,
|
|
bareOsListThemeNames,
|
|
bareParseLsColors,
|
|
bareSerializeLsColors,
|
|
bareLsColorOpenSgrFromMap,
|
|
bareDefaultDircolorsDatabase,
|
|
bareParseDircolorsDatabase,
|
|
BARE_OS_CTX_API_VERSION,
|
|
buildBareOsRuntimeCaps,
|
|
bareOsBareModulesEnabled,
|
|
bareOsBareModuleManifestEmbeddedRef,
|
|
buildBareCtxObjectFromHost,
|
|
buildPearCtxObjectFromHost,
|
|
loadBareModuleManifest,
|
|
maybeMergeBareFromDrive,
|
|
maybeBareOsBuildBinManifest,
|
|
createFishReadLine,
|
|
fuzzyMatch,
|
|
stripAnsi,
|
|
parseHistoryFile,
|
|
formatHistoryFile,
|
|
dedupeConsecutiveHistory,
|
|
searchHistoryEntries,
|
|
shouldPersistShellHistoryCommand,
|
|
scrubShellHistoryEntries,
|
|
SHELL_BUILTINS,
|
|
resolveShellPromptHookSegment,
|
|
parseCompletionContext,
|
|
rankCompletionItems,
|
|
suggestGhostFromHistory,
|
|
suggestGhostFromFs,
|
|
ghostReplaceCurrentWord,
|
|
longestCommonCompletionPrefix,
|
|
completeLine,
|
|
levenshtein,
|
|
fieldMatches,
|
|
dowFieldMatches,
|
|
parseCronLine,
|
|
jobMatchesDate,
|
|
bareInitdReadinessSnapshot,
|
|
bareInitdShutdownActiveUnitsReverse,
|
|
getBareServiceRuntime,
|
|
registerBareInitdDisposer,
|
|
registerKernelShutdownHook,
|
|
runKernelShutdownHooks,
|
|
startBareInitd,
|
|
stopBareInitd,
|
|
getLastBareInitdDagSnapshotJson,
|
|
appendVarLog,
|
|
ensureBareOsVarLogTree,
|
|
KERNEL_CONSOLE_LOG,
|
|
BOOT_LOG,
|
|
bareOsKernelMetricsReset,
|
|
bareOsKernelMetricsSnapshot,
|
|
parseUnitDropInText,
|
|
bareOsHyperswarmOptsFromEnv,
|
|
bareOsBootJoinPeerHexFromEnv,
|
|
bareOsBootInitPrefetchEnabled,
|
|
bareOsBootWarmBinDownloadDiffEnabled,
|
|
bareOsProcessTableSnapshot,
|
|
buildBareOsSyscallsProcJson,
|
|
bareOsParseSendmsgScmRights,
|
|
wantPosixSocketScmRights,
|
|
parseBareOsHrpcAllowlistJson,
|
|
bareOsHrpcAllowlistDeniesRoute,
|
|
buildBareOsProtomuxExtensionsProcJson,
|
|
createBareOsDiskOsBridge,
|
|
buildBareOsReplicationLiveSketch,
|
|
evaluateBareOsPeerAdmission,
|
|
bareOsFormatPeerAdmissionAuditEvent,
|
|
bareOsPeerAdmissionAuditRateAllow,
|
|
parseDelegateAllowSet,
|
|
isDelegateKindAllowed,
|
|
bareOsAuditAppend,
|
|
bareOsAuditVerifyRows,
|
|
bareOsDrainFcntlLockWaiters,
|
|
buildBareOsReplicationOperatorSurfaceProcJson,
|
|
DEFAULT_CURL_USER_AGENT,
|
|
DEFAULT_WGET_USER_AGENT,
|
|
filenameFromContentDisposition,
|
|
encodeUstarHeader,
|
|
runTarCli,
|
|
require,
|
|
__dirname,
|
|
TAR_BLK,
|
|
concatTarParts,
|
|
headerFromInit,
|
|
testCorestoreDir,
|
|
testCtx,
|
|
personalHomeBacking,
|
|
readBuiltBin
|
|
} from './test/_helpers.js'
|
|
|
|
test('tokenize handles quotes and ops', async (t) => {
|
|
const tok = tokenize('ls -la | cat > out')
|
|
t.ok(tok.some((x) => x.type === 'op' && x.value === '|'))
|
|
t.ok(tok.some((x) => x.type === 'op' && x.value === '>'))
|
|
const w = tokenize("echo 'a b'")
|
|
const words = w.filter((x) => x.type === 'word').map((x) => x.value)
|
|
t.is(words.join(','), 'echo,a b')
|
|
})
|
|
|
|
test('tokenize records quote parts for glob (literal * in quotes)', async (t) => {
|
|
const u = tokenize('echo *')
|
|
const uwords = u.filter((x) => x.type === 'word')
|
|
const star = uwords[uwords.length - 1]
|
|
t.ok(star && star.type === 'word' && star.parts)
|
|
t.is(star.parts.length, 1)
|
|
t.is(star.parts[0].q, 'u')
|
|
t.is(star.parts[0].t, '*')
|
|
const sq = tokenize("echo '*'")
|
|
const sqw = sq.filter((x) => x.type === 'word')
|
|
t.is(sqw[0].value, 'echo')
|
|
const lit = sqw[1]
|
|
t.is(lit.parts.length, 1)
|
|
t.is(lit.parts[0].q, 's')
|
|
t.is(lit.parts[0].t, '*')
|
|
})
|
|
|
|
test('tokenize keeps arithmetic expansion in one word token', async (t) => {
|
|
const toks = tokenize('n=$((n+1)); echo "$((n+2))"')
|
|
const words = toks.filter((x) => x.type === 'word').map((x) => x.value)
|
|
t.ok(words.includes('n=$((n+1))'))
|
|
t.ok(words.includes('$((n+2))'))
|
|
t.is(toks.filter((x) => x.type === 'op' && x.value === '(').length, 0)
|
|
t.is(toks.filter((x) => x.type === 'op' && x.value === ')').length, 0)
|
|
})
|
|
|
|
test('tokenize recognizes <<- heredoc operator', async (t) => {
|
|
const toks = tokenize('cat <<- EOF')
|
|
t.ok(toks.some((x) => x.type === 'op' && x.value === '<<-'))
|
|
})
|
|
|
|
test('tokenize emits compound ;; ;& |& >& operators', (t) => {
|
|
const a = tokenize('echo ok;; echo x')
|
|
t.ok(a.some((x) => x.type === 'op' && x.value === ';;'))
|
|
t.ok(tokenize('a;&b').some((x) => x.type === 'op' && x.value === ';&'))
|
|
t.ok(tokenize('a|&b').some((x) => x.type === 'op' && x.value === '|&'))
|
|
t.ok(tokenize('a>&2').some((x) => x.type === 'op' && x.value === '>&'))
|
|
})
|
|
|
|
test('decodeBareOsDollarQuote handles ANSI escapes', (t) => {
|
|
t.is(decodeBareOsDollarQuote(String.raw`a\n\t\\'\x41`), "a\n\t\\'A")
|
|
})
|
|
|
|
test('tokenize parses $\'…\' as decoded single-quoted segment', (t) => {
|
|
const toks = tokenize(`echo $'hi\\nx'`)
|
|
const echoTok = toks.find((x) => x.type === 'word' && x.value.includes('hi'))
|
|
t.ok(echoTok && echoTok.parts)
|
|
const sq = echoTok.parts.filter((p) => p.q === 's')
|
|
t.ok(sq.some((p) => p.t === 'hi\nx'))
|
|
})
|
|
|
|
test('execShellLine $\'…\' echo and cmdsubst $(…) / backticks when enabled', async (t) => {
|
|
const dir = testCorestoreDir('shansi')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sans'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push('e:' + a.join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_CMDSUBST = '1'
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, `echo $'a\\tb'`)
|
|
t.ok(logs.some((l) => l === 'a\tb'), logs.join('|'))
|
|
logs.length = 0
|
|
await execShellLine(ctx, 'echo "$(echo hi)"')
|
|
t.ok(logs.some((l) => l.includes('hi')), logs.join('|'))
|
|
logs.length = 0
|
|
await execShellLine(ctx, 'echo "`echo lo`"')
|
|
t.ok(logs.some((l) => l.includes('lo')), logs.join('|'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine command substitution is enabled by default', async (t) => {
|
|
const dir = testCorestoreDir('shcmdsub-default')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pcmdsubdef'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
delete ctx.vfs.env.BARE_OS_SHELL_CMDSUBST
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push('e:' + a.join(' '))
|
|
}
|
|
await execShellLine(ctx, 'echo "$(echo hi)"')
|
|
t.ok(logs.some((l) => l.includes('hi')), logs.join('|'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine brace range expansion supports {1..5}', async (t) => {
|
|
const dir = testCorestoreDir('sh-brace-range')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pbrace'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_BRACE_EXPANSION = '1'
|
|
ctx.console = { log: (m) => lines.push(String(m)), error: () => {} }
|
|
await execShellLine(ctx, 'echo {1..5}')
|
|
const joined = lines.join('|')
|
|
t.ok(!joined.includes('1..5'), joined)
|
|
t.ok(joined.includes('1'), joined)
|
|
t.ok(joined.includes('5'), joined)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine arithmetic command form (( 2 + 3 ))', async (t) => {
|
|
const dir = testCorestoreDir('sh-arith-cmd')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('parithcmd'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
await execShellLine(ctx, '(( 2 + 3 ))')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('expandWord supports ${#var} length and ${var:offset} when v3', (t) => {
|
|
const env = {
|
|
X: 'abcde',
|
|
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
|
BARE_OS_SHELL_PARAM_EXPANSION_V3: '1'
|
|
}
|
|
t.is(expandWord('${#X}', env), '5')
|
|
t.is(expandWord('${X:2}', env), 'cde')
|
|
t.is(expandWord('${X:2:2}', env), 'cd')
|
|
})
|
|
|
|
test('expandWord indirect ${!a} when enabled', (t) => {
|
|
const env = {
|
|
ref: 'X',
|
|
X: 'hi',
|
|
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
|
BARE_OS_SHELL_INDIRECT_EXPANSION: '1'
|
|
}
|
|
t.is(expandWord('${!ref}', env), 'hi')
|
|
})
|
|
|
|
test('tokenizeBareShellLineDetailed records span and mode metadata', async (t) => {
|
|
const rows = tokenizeBareShellLineDetailed(`echo 'a b' "c d" $((1+2)) <<EOF`)
|
|
t.ok(rows.length >= 4)
|
|
t.ok(rows.some((r) => r.mode === 'single'))
|
|
t.ok(rows.some((r) => r.mode === 'double'))
|
|
t.ok(rows.some((r) => r.mode === 'arith' || r.value.includes('))')))
|
|
t.ok(rows.every((r) => Number.isInteger(r.start) && Number.isInteger(r.end)))
|
|
t.ok(rows.every((r) => r.end >= r.start))
|
|
})
|
|
|
|
test('tokenizeBareShellLineDetailed deterministic output', async (t) => {
|
|
const src = `if [ "$x" = "y" ]; then echo ok; fi`
|
|
const a = JSON.stringify(tokenizeBareShellLineDetailed(src))
|
|
const b = JSON.stringify(tokenizeBareShellLineDetailed(src))
|
|
t.is(a, b)
|
|
})
|
|
|
|
test('bareOsShellAstSnapshot produces stable schema and tokenized pipeline', async (t) => {
|
|
const snap = bareOsShellAstSnapshot('echo hi | cat')
|
|
t.is(snap.schema, 1)
|
|
t.is(snap.line, 'echo hi | cat')
|
|
t.ok(Array.isArray(snap.tokens) && snap.tokens.length >= 3)
|
|
t.ok(Array.isArray(snap.pipeline) && snap.pipeline.length === 2)
|
|
t.ok(Array.isArray(snap.diagnosticTokens) && snap.diagnosticTokens.length >= 3)
|
|
})
|
|
|
|
test('planShellRedirections returns normalized redirects', async (t) => {
|
|
const p = planShellRedirections({
|
|
redirIn: null,
|
|
redirOut: { type: 'word', value: '/tmp/o', parts: [] },
|
|
redirAppend: false,
|
|
redirErr: null,
|
|
redirErrAppend: false,
|
|
mergeStderrToStdout: true,
|
|
redirHereDoc: null
|
|
})
|
|
t.is(p.stdin, 'inherit')
|
|
t.is(p.stdout, 'truncate')
|
|
t.is(p.stderr, 'stdout')
|
|
})
|
|
|
|
test('buildShellExecutionGraph models lists and pipelines', async (t) => {
|
|
const g = buildShellExecutionGraph('echo a | cat && echo b; echo c')
|
|
t.is(g.schema, 1)
|
|
t.is(g.listCount, 2)
|
|
t.ok(Array.isArray(g.lists) && g.lists.length === 2)
|
|
t.is(g.lists[0].segments[0].pipelineLength, 2)
|
|
t.is(g.lists[0].andOrOps[0], '&&')
|
|
})
|
|
|
|
test('execShellLine expansion trace captures stages when enabled', async (t) => {
|
|
const dir = testCorestoreDir('shexpansiontrace')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pexpansiontrace'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_EXPANSION_TRACE = '1'
|
|
ctx.vfs.env.BARE_OS_SHELL_PARAM_EXPANSION = '1'
|
|
await execShellLine(ctx, 'A=ok; echo ${A:-no}')
|
|
t.ok(Array.isArray(ctx.shellExpansionTrace))
|
|
t.ok(ctx.shellExpansionTrace.some((r) => r.stage === 'expand-pre'))
|
|
t.ok(ctx.shellExpansionTrace.some((r) => r.stage === 'split-glob'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine stores execution graph when dump mode is enabled', async (t) => {
|
|
const dir = testCorestoreDir('shexecgraphdump')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pexecgraphdump'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`)
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_EXEC_GRAPH_DUMP = '1'
|
|
await execShellLine(ctx, 'echo one | echo two && echo three')
|
|
t.ok(ctx.shellLastExecGraph && ctx.shellLastExecGraph.schema === 1)
|
|
t.ok(ctx.shellLastExecGraph.listCount >= 1)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine test [ builtins and $(( $var + … ))', async (t) => {
|
|
const dir = testCorestoreDir('shbrackettest')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshbrk'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/test', b4a.from(await readBuiltBin('test')))
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.runBinCommand = function (argv, ro) {
|
|
return runBinCommand(this, argv, ro)
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'a=1; test 2 -eq $((a+1))')
|
|
t.is(ctx.exitCode, 0)
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'a=1; [ 2 -eq $(($a+1)) ]')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('expandWord arithmetic honors $var inside $(( ))', (t) => {
|
|
t.is(expandWord('$(( $a + 1 ))', { a: '2' }), '3')
|
|
t.is(expandWord('$(( $a+1 ))', { a: '2' }), '3')
|
|
})
|
|
|
|
test('expandWord expansion-depth guard env is accepted', (t) => {
|
|
const env = {
|
|
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
|
BARE_OS_SHELL_PARAM_EXPANSION_V2: '1',
|
|
BARE_OS_SHELL_EXPANSION_MAX_DEPTH: '1'
|
|
}
|
|
t.is(expandWord('${A:=x}', env), 'x')
|
|
})
|
|
|
|
test('expandWord strict POSIX arithmetic emits strict-token diagnostics', (t) => {
|
|
const env = { BARE_OS_SHELL_POSIX_MODE: '1' }
|
|
t.exception(
|
|
() => expandWord('$((1 + bad,2))', env),
|
|
/POSIX mode strict arithmetic/
|
|
)
|
|
})
|
|
|
|
test('execShellLine pathname glob and noglob', async (t) => {
|
|
const dir = testCorestoreDir('shglob')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sglob'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const echo = `
|
|
async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`
|
|
await drive.put('/bin/echo', b4a.from(echo))
|
|
const ctx = testCtx(drive, personal)
|
|
await ctx.vfs.writeFile('/home/user/a.txt', b4a.from('x'))
|
|
await ctx.vfs.writeFile('/home/user/b.txt', b4a.from('y'))
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
await ctx.vfs.chdir('/home/user')
|
|
await execShellLine(ctx, 'echo *.txt')
|
|
t.ok(
|
|
logs.some((l) => l.includes('a.txt') && l.includes('b.txt')),
|
|
logs.join('|')
|
|
)
|
|
logs.length = 0
|
|
await execShellLine(ctx, "echo '*.txt'")
|
|
t.ok(logs.some((l) => l === '*.txt'), logs.join('|'))
|
|
logs.length = 0
|
|
ctx.vfs.env.BARE_OS_SHELL_NOGLOB = '1'
|
|
await execShellLine(ctx, 'echo *.txt')
|
|
t.ok(logs.some((l) => l === '*.txt'))
|
|
delete ctx.vfs.env.BARE_OS_SHELL_NOGLOB
|
|
await execShellLine(ctx, 'set -f; echo *.txt')
|
|
t.ok(logs.some((l) => l === '*.txt'))
|
|
await execShellLine(ctx, 'set +f; echo *.txt')
|
|
t.ok(logs.some((l) => l.includes('a.txt')))
|
|
logs.length = 0
|
|
await execShellLine(ctx, 'echo ~user')
|
|
t.ok(logs.some((l) => l === '/home/user'), logs.join('|'))
|
|
logs.length = 0
|
|
await execShellLine(ctx, 'echo ~other')
|
|
t.ok(logs.some((l) => l === '/home/other'), logs.join('|'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_POSIX_MODE grouped list', async (t) => {
|
|
const dir = testCorestoreDir('shposixgroup')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('spg'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const echo = `
|
|
async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`
|
|
await drive.put('/bin/echo', b4a.from(echo))
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_POSIX_MODE = '1'
|
|
await ctx.vfs.chdir('/home/user')
|
|
await execShellLine(ctx, '( echo hi )')
|
|
t.ok(logs.some((l) => l === 'hi'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_GROUPING runs parenthesized list without POSIX mode', async (t) => {
|
|
const dir = testCorestoreDir('shgroupingonly')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sgo'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const echo = `
|
|
async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`
|
|
await drive.put('/bin/echo', b4a.from(echo))
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
delete ctx.vfs.env.BARE_OS_SHELL_POSIX_MODE
|
|
ctx.vfs.env.BARE_OS_SHELL_GROUPING = '1'
|
|
await ctx.vfs.chdir('/home/user')
|
|
await execShellLine(ctx, '( echo hi )')
|
|
t.ok(logs.some((l) => l === 'hi'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine select is unsupported with clear error', async (t) => {
|
|
const dir = testCorestoreDir('shselectstub')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sels'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'select x in a b; do echo x; done')
|
|
t.ok(logs.some((l) => l.includes('select') && l.includes('unsupported')))
|
|
t.is(ctx.exitCode, 2)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine rejects stray reserved word at statement start', async (t) => {
|
|
const dir = testCorestoreDir('shmisres')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('smr'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'then echo x')
|
|
t.ok(logs.some((l) => l.includes("reserved word 'then'")))
|
|
t.is(ctx.exitCode, 2)
|
|
logs.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'fi')
|
|
t.ok(logs.some((l) => l.includes("reserved word 'fi'")))
|
|
t.is(ctx.exitCode, 2)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine [[ rejects by default and supports == when gated', async (t) => {
|
|
const dir = testCorestoreDir('shdblbr')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sdb'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, '[[ a == a ]]')
|
|
t.ok(logs.some((l) => l.includes('not supported')))
|
|
t.is(ctx.exitCode, 2)
|
|
logs.length = 0
|
|
ctx.vfs.env.BARE_OS_SHELL_DOUBLE_BRACKET = '1'
|
|
await execShellLine(ctx, '[[ x == x ]]')
|
|
t.is(ctx.exitCode, 0)
|
|
await execShellLine(ctx, '[[ x == y ]]')
|
|
t.is(ctx.exitCode, 1)
|
|
await execShellLine(ctx, '[[ x != y ]]')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine applies alias expansion before shell function dispatch', async (t) => {
|
|
const dir = testCorestoreDir('shaliasfn')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('safn'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log(s) {
|
|
lines.push(String(s))
|
|
},
|
|
error(s) {
|
|
lines.push('e:' + String(s))
|
|
}
|
|
}
|
|
ctx.exitCode = 0
|
|
|
|
await execShellLine(
|
|
ctx,
|
|
"bos_fn_a() { echo FN; }; alias bos_fn_a='echo AL'; bos_fn_a"
|
|
)
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(lines.includes('AL'))
|
|
t.ok(!lines.includes('FN'))
|
|
|
|
lines.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(
|
|
ctx,
|
|
"bos_fn_b() { echo FB; }; alias bos_call_b='bos_fn_b'; bos_call_b"
|
|
)
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(lines.includes('FB'))
|
|
|
|
lines.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'bos_fn_c() { echo FC; }; bos_fn_c')
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(lines.includes('FC'))
|
|
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine rejects process substitution with exit 2', async (t) => {
|
|
const dir = testCorestoreDir('shprosubst')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('psub'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'echo hi <(echo x)')
|
|
t.ok(logs.some((l) => l.includes('process substitution')))
|
|
t.is(ctx.exitCode, 2)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('expandWord reads env', async (t) => {
|
|
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
|
|
})
|
|
|
|
test('expandWord $? and ${?} use BARE_OS_EXIT_STATUS', async (t) => {
|
|
const env = { [BARE_OS_EXIT_STATUS_ENV]: '7', HOME: '/h' }
|
|
t.is(expandWord('code=$?', env), 'code=7')
|
|
t.is(expandWord('c=${?}', env), 'c=7')
|
|
t.is(expandWord('missing=$?', {}), 'missing=0')
|
|
})
|
|
|
|
test('expandWord param expansion v2 (## %% :=)', async (t) => {
|
|
const base = {
|
|
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
|
BARE_OS_SHELL_PARAM_EXPANSION_V2: '1',
|
|
P: '/usr/bin/foo',
|
|
Q: 'caba'
|
|
}
|
|
t.is(expandWord('${P##*/}', base), 'foo')
|
|
t.is(expandWord('${P#*/}', base), 'usr/bin/foo')
|
|
t.is(expandWord('${Q%%ba}', base), 'ca')
|
|
t.is(expandWord('${Q%ba}', base), 'ca')
|
|
const assign = { ...base, EMPTY: '' }
|
|
t.is(expandWord('${EMPTY:=set}', assign), 'set')
|
|
t.is(assign.EMPTY, 'set')
|
|
})
|
|
|
|
/** POSIX-style parameter expansion vectors (Issue 7 subset; env-gated V3). */
|
|
test('expandWord param expansion v3 (:- :+ :?)', async (t) => {
|
|
const base = {
|
|
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
|
BARE_OS_SHELL_PARAM_EXPANSION_V3: '1',
|
|
HOME: '/home/u'
|
|
}
|
|
t.is(expandWord('${UNSET:-/default}', base), '/default')
|
|
t.is(expandWord('${EMPTY:-/alt}', { ...base, EMPTY: '' }), '/alt')
|
|
t.is(expandWord('${SET:-ignored}', { ...base, SET: 'ok' }), 'ok')
|
|
t.is(expandWord('${SET:+present}', { ...base, SET: 'v' }), 'present')
|
|
t.is(expandWord('${UNSET:+absent}', base), '')
|
|
t.exception(
|
|
() => expandWord('${UNSET:?hard fail}', base),
|
|
/hard fail|parameter null or unset/
|
|
)
|
|
t.exception(
|
|
() => expandWord('${EMPTY:?msg}', { ...base, EMPTY: '' }),
|
|
/msg|parameter null or unset/
|
|
)
|
|
})
|
|
|
|
test('syncBareOsExitStatusEnv and execShellLine update env', async (t) => {
|
|
const dir = testCorestoreDir('exstat')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 1 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/true',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 0 }`)
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.exitCode = 0
|
|
syncBareOsExitStatusEnv(ctx)
|
|
t.is(ctx.vfs.env[BARE_OS_EXIT_STATUS_ENV], '0')
|
|
await execShellLine(ctx, 'false')
|
|
t.is(ctx.vfs.env[BARE_OS_EXIT_STATUS_ENV], '1')
|
|
await execShellLine(ctx, 'true')
|
|
t.is(ctx.vfs.env[BARE_OS_EXIT_STATUS_ENV], '0')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine export -p lists session env', async (t) => {
|
|
const dir = testCorestoreDir('shexportp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pexp'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
ctx.vfs.env.FOO = 'bar'
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'export -p')
|
|
t.ok(logs.some((l) => l.includes("export FOO='bar'")))
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine readonly -p lists readonly vars with stable quoting', async (t) => {
|
|
const dir = testCorestoreDir('shreadonlyp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pronly'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, "readonly FOO=bar BAR='x y'")
|
|
await execShellLine(ctx, 'readonly -p')
|
|
t.ok(logs.some((l) => l.includes("readonly BAR='x y'")))
|
|
t.ok(logs.some((l) => l.includes("readonly FOO='bar'")))
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_READ_BUILTIN reads from shellStdin', async (t) => {
|
|
const dir = testCorestoreDir('shreadin')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshr'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_READ_BUILTIN = '1'
|
|
ctx.vfs.env.IFS = ' '
|
|
ctx.shellStdin = 'one two\n'
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'read x y')
|
|
t.is(ctx.vfs.env.x, 'one')
|
|
t.is(ctx.vfs.env.y, 'two')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine read builtin supports -d delimiter', async (t) => {
|
|
const dir = testCorestoreDir('shread-delim')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshread-delim'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_READ_BUILTIN = '1'
|
|
ctx.shellStdin = 'alpha,beta,gamma'
|
|
await execShellLine(ctx, 'read -d , first rest')
|
|
t.is(ctx.vfs.env.first, 'alpha')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine read builtin supports -r raw backslashes', async (t) => {
|
|
const dir = testCorestoreDir('shread-raw')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshread-raw'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_READ_BUILTIN = '1'
|
|
ctx.shellStdin = 'a\\ b\n'
|
|
await execShellLine(ctx, 'read cooked')
|
|
t.is(ctx.vfs.env.cooked, 'a b')
|
|
ctx.shellStdin = 'c\\ d\n'
|
|
await execShellLine(ctx, 'read -r raw')
|
|
t.is(ctx.vfs.env.raw, 'c\\ d')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine read builtin timeout returns non-zero', async (t) => {
|
|
const dir = testCorestoreDir('shread-timeout')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshread-timeout'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_READ_BUILTIN = '1'
|
|
ctx.readLine = async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
return 'late'
|
|
}
|
|
await execShellLine(ctx, 'read -t 0.01 x')
|
|
t.is(ctx.exitCode, 1)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine here-doc works in non-interactive script mode', async (t) => {
|
|
const dir = testCorestoreDir('sh-heredoc-script')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshhd'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/cat', b4a.from(await readBuiltBin('cat')))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = { log: (s) => lines.push(String(s)), error() {} }
|
|
ctx.readLine = (() => {
|
|
const seq = ['one $HOME', 'two', 'EOF']
|
|
return () => Promise.resolve(seq.shift() ?? null)
|
|
})()
|
|
ctx.vfs.env.HOME = '/home/user'
|
|
ctx.vfs.env.BARE_OS_SHELL_POSIX_MODE = '1'
|
|
await execShellLine(ctx, 'cat << EOF')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_ERREXIT skips after failed command', async (t) => {
|
|
const dir = testCorestoreDir('errexit')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex-e'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 1 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/true',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 0 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(
|
|
`async function run(ctx, argv) { ctx.console.log(argv.slice(1).join(' ')) }`
|
|
)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_ERREXIT = '1'
|
|
await execShellLine(ctx, 'false; echo AFTER')
|
|
t.ok(!logs.some((l) => l.includes('AFTER')))
|
|
logs.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'true; echo OK')
|
|
t.ok(logs.some((l) => l.includes('OK')))
|
|
logs.length = 0
|
|
ctx.exitCode = 0
|
|
delete ctx.vfs.env.BARE_OS_SHELL_ERREXIT
|
|
await execShellLine(ctx, 'set -e; false; echo NEVER')
|
|
t.ok(!logs.some((l) => l.includes('NEVER')))
|
|
logs.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'set -e; if true; then false; echo NEVER1; fi; echo NEVER2')
|
|
t.ok(!logs.some((l) => l.includes('NEVER1')))
|
|
t.ok(!logs.some((l) => l.includes('NEVER2')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine trap normalizes signals and prints handlers', async (t) => {
|
|
const dir = testCorestoreDir('sh-trap')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshtrap'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
await execShellLine(ctx, "trap 'trap -l' SIGTERM")
|
|
t.is(ctx.shellTrapHandlers.TERM, 'trap -l')
|
|
await execShellLine(ctx, 'trap - TERM')
|
|
t.is(ctx.shellTrapHandlers.TERM, undefined)
|
|
await execShellLine(ctx, "trap 'trap -l' TERM")
|
|
logs.length = 0
|
|
await execShellLine(ctx, 'trap -p')
|
|
t.ok(logs.some((l) => l.includes("trap -- 'trap -l' TERM")))
|
|
logs.length = 0
|
|
const ran = await dispatchShellTrapSignal(ctx, 'SIGTERM')
|
|
t.is(ran, true)
|
|
t.ok(logs.some((l) => l.includes('HUP INT KILL TERM')))
|
|
const missed = await dispatchShellTrapSignal(ctx, 'SIGUSR1')
|
|
t.is(missed, false)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine trap EXIT runs before exit', async (t) => {
|
|
const dir = testCorestoreDir('sh-trap-exit')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pshtrapexit'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
await execShellLine(ctx, "trap 'trap -l' EXIT; exit 1")
|
|
t.ok(logs.some((l) => l.includes('HUP INT KILL TERM')))
|
|
t.is(ctx.exitCode, 1)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_NOUNSET rejects unbound variable', async (t) => {
|
|
const dir = testCorestoreDir('nounset')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex-nu'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
const errs = []
|
|
ctx.console.error = (m) => errs.push(String(m))
|
|
ctx.vfs.env.BARE_OS_SHELL_NOUNSET = '1'
|
|
await execShellLine(ctx, 'echo $UNBOUND_XYZ')
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(errs.some((e) => /unbound variable/.test(e)))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine BARE_OS_SHELL_PIPEFAIL uses first failing stage exit', async (t) => {
|
|
const dir = testCorestoreDir('pipefail')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex-pf'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 1 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/true',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 0 }`)
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_PIPEFAIL = '1'
|
|
await execShellLine(ctx, 'false | true')
|
|
t.is(ctx.exitCode, 1)
|
|
delete ctx.vfs.env.BARE_OS_SHELL_PIPEFAIL
|
|
await execShellLine(ctx, 'false | true')
|
|
t.is(ctx.exitCode, 0)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine set -o pipefail and BARE_OS_PIPESTATUS', async (t) => {
|
|
const dir = testCorestoreDir('pipestatus')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex-ps'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 1 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/true',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 0 }`)
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.vfs.env.BARE_OS_SHELL_PIPESTATUS = '1'
|
|
await execShellLine(ctx, 'set -o pipefail; true | false | true')
|
|
t.is(ctx.vfs.env.BARE_OS_PIPESTATUS, '0 1 0')
|
|
t.is(ctx.exitCode, 1)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand kill resolves %n and %% via shellBackgroundJobs', async (t) => {
|
|
const dir = testCorestoreDir('killjob')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pkjob'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const killPath = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../../kernel/bin/kill'
|
|
)
|
|
const killSrc = await readFile(killPath)
|
|
await drive.put('/bin/kill', killSrc)
|
|
|
|
const ctx = testCtx(drive, personal)
|
|
/** @type {[string, string][]} */
|
|
const calls = []
|
|
ctx.bareOsSendSignal = function (target, sig) {
|
|
calls.push([String(target), String(sig)])
|
|
}
|
|
ctx.shellBackgroundJobs = {
|
|
nextId: 3,
|
|
list: [
|
|
{
|
|
id: 1,
|
|
pgid: 400,
|
|
done: false,
|
|
stopped: false,
|
|
label: 'a',
|
|
promise: Promise.resolve('ok')
|
|
},
|
|
{
|
|
id: 2,
|
|
pgid: 401,
|
|
done: false,
|
|
stopped: false,
|
|
label: 'b',
|
|
promise: Promise.resolve('ok')
|
|
}
|
|
]
|
|
}
|
|
ctx.exitCode = 0
|
|
await runBinCommand(ctx, ['kill', '-0', '%1'])
|
|
t.alike(calls[0], ['4101', '0'])
|
|
calls.length = 0
|
|
await runBinCommand(ctx, ['kill', '-TERM', '%%'])
|
|
t.alike(calls[0], ['4102', 'TERM'])
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine set -o prints shell toggles', async (t) => {
|
|
const dir = testCorestoreDir('seto-print')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pso'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'set -o')
|
|
t.ok(logs.some((l) => /errexit\s+off/.test(l)))
|
|
t.ok(logs.some((l) => /pipefail\s+off/.test(l)))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine jobs -p prints pgid only', async (t) => {
|
|
const dir = testCorestoreDir('jobs-p')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pjobs'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.shellBackgroundJobs = {
|
|
nextId: 2,
|
|
list: [
|
|
{
|
|
id: 1,
|
|
pgid: 712,
|
|
sid: 1,
|
|
done: false,
|
|
stopped: false,
|
|
label: 'x',
|
|
promise: Promise.resolve('ok')
|
|
}
|
|
]
|
|
}
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'jobs -p')
|
|
t.ok(logs.includes('712'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine jobs -l is stable and parseable', async (t) => {
|
|
const dir = testCorestoreDir('jobs-l-parseable')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pjobsl'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.shellBackgroundJobs = {
|
|
nextId: 3,
|
|
list: [
|
|
{
|
|
id: 2,
|
|
pgid: 990,
|
|
sid: 1,
|
|
done: false,
|
|
stopped: false,
|
|
label: 'sleep 1',
|
|
promise: Promise.resolve('ok')
|
|
}
|
|
]
|
|
}
|
|
await execShellLine(ctx, 'jobs -l')
|
|
t.ok(logs.some((l) => /^\[2\]\+\s+Running/.test(l)))
|
|
t.ok(logs.some((l) => l.includes('pid=4102')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine wait supports synthetic pid and all keyword', async (t) => {
|
|
const dir = testCorestoreDir('wait-pid-all')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pwaitpidall'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const mkJob = (id, exitCode) => ({
|
|
id,
|
|
pgid: 700 + id,
|
|
sid: 1,
|
|
done: false,
|
|
stopped: false,
|
|
label: `job-${id}`,
|
|
lastExitCode: exitCode,
|
|
promise: Promise.resolve('ok')
|
|
})
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.shellBackgroundJobs = { nextId: 3, list: [mkJob(1, 0), mkJob(2, 7)] }
|
|
await execShellLine(ctx, 'wait 4102')
|
|
t.is(ctx.exitCode, 7)
|
|
await execShellLine(ctx, 'wait all')
|
|
t.is(ctx.exitCode, 7)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine deny/allow policy and sandbox guard', async (t) => {
|
|
const dir = testCorestoreDir('sh-policy-sandbox')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('ppolicysandbox'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/echo', b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`))
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_DENY_COMMANDS = 'echo'
|
|
await execShellLine(ctx, 'echo hi')
|
|
t.is(ctx.exitCode, 126)
|
|
t.ok(logs.some((l) => l.includes('command denied by policy')))
|
|
delete ctx.vfs.env.BARE_OS_SHELL_DENY_COMMANDS
|
|
ctx.vfs.env.BARE_OS_SHELL_SANDBOX = '1'
|
|
await execShellLine(ctx, 'echo hi')
|
|
t.is(ctx.exitCode, 126)
|
|
t.ok(logs.some((l) => l.includes('sandbox blocks external command')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine unsafe redirect guard rejects risky paths', async (t) => {
|
|
const dir = testCorestoreDir('sh-redirect-guard')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('predirectguard'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/echo', b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`))
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_REDIRECT_GUARD = '1'
|
|
await execShellLine(ctx, 'echo hi > /proc/self/environ')
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(logs.some((l) => l.includes('unsafe stdout redirect path denied')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine redirect write to /proc/cpuinfo is visible nonzero', async (t) => {
|
|
const dir = testCorestoreDir('sh-proc-write-deny')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pprocwrite'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
await execShellLine(ctx, 'echo hi > /proc/cpuinfo')
|
|
t.ok(ctx.exitCode !== 0)
|
|
t.ok(logs.some((l) => /EACCES|EROFS|denied|read-only/i.test(l)))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine appends structured shell audit events', async (t) => {
|
|
const dir = testCorestoreDir('sh-audit-events')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pauditevents'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/echo', b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`))
|
|
const ctx = testCtx(drive, personal)
|
|
await execShellLine(ctx, 'echo hello')
|
|
t.ok(Array.isArray(ctx.shellAuditEvents))
|
|
t.ok(ctx.shellAuditEvents.some((e) => e.event === 'shell.command.start'))
|
|
t.ok(ctx.shellAuditEvents.some((e) => e.event === 'shell.command.finish'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine errexit stops inside if-then body after failure', async (t) => {
|
|
const dir = testCorestoreDir('errexit-if')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pex-eif'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`async function run(ctx) { ctx.exitCode = 1 }`)
|
|
)
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(
|
|
`async function run(ctx, argv) { ctx.console.log(argv.slice(1).join(' ')) }`
|
|
)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (m) => logs.push(String(m)),
|
|
error: (...a) => logs.push(a.map(String).join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_ERREXIT = '1'
|
|
await execShellLine(ctx, 'if true; then false; echo BAD; fi')
|
|
t.ok(!logs.some((l) => l.includes('BAD')))
|
|
logs.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'if false; then echo no; fi; echo AFTER')
|
|
t.ok(logs.some((l) => l.includes('AFTER')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('evaluateBareOsPeerAdmission honors dhtAddressClass with allowlist', async (t) => {
|
|
const env = {
|
|
BARE_OS_PEER_ALLOWLIST_HEX: '',
|
|
BARE_OS_PEER_ALLOW_ALL: '1',
|
|
BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST: 'ipv4,relay'
|
|
}
|
|
const deny = evaluateBareOsPeerAdmission(env, 'aa', {
|
|
dhtAddressClass: 'ipv6'
|
|
})
|
|
t.is(deny.schema, 2)
|
|
t.is(deny.verdict, 'deny')
|
|
const ok = evaluateBareOsPeerAdmission(env, 'aa', { dhtAddressClass: 'ipv4' })
|
|
t.is(ok.schema, 2)
|
|
t.is(ok.verdict, 'allow')
|
|
})
|
|
|
|
test('evaluateBareOsPeerAdmission denylist wins over allowlist', async (t) => {
|
|
const env = {
|
|
BARE_OS_PEER_ALLOWLIST_HEX: 'aa,bb',
|
|
BARE_OS_PEER_DENYLIST_HEX: 'bb'
|
|
}
|
|
t.is(evaluateBareOsPeerAdmission(env, 'bb').verdict, 'deny')
|
|
t.is(evaluateBareOsPeerAdmission(env, 'bb').reason, 'peer_denylist')
|
|
t.is(evaluateBareOsPeerAdmission(env, 'aa').verdict, 'allow')
|
|
})
|
|
|
|
test('evaluateBareOsPeerAdmission strict empty allowlist denies all peers', async (t) => {
|
|
const env = {
|
|
BARE_OS_PEER_ALLOWLIST_HEX: '',
|
|
BARE_OS_PEER_ALLOWLIST_STRICT: '1'
|
|
}
|
|
const r = evaluateBareOsPeerAdmission(env, 'abc')
|
|
t.is(r.verdict, 'deny')
|
|
t.ok(String(r.note || '').includes('allowlist is empty'))
|
|
})
|
|
|
|
test('evaluateBareOsPeerAdmission BARE_OS_PEER_REQUIRE_CAPS_JSON', async (t) => {
|
|
const env = {
|
|
BARE_OS_PEER_ALLOW_ALL: '1',
|
|
BARE_OS_PEER_REQUIRE_CAPS_JSON: '["seed","read"]'
|
|
}
|
|
t.is(
|
|
evaluateBareOsPeerAdmission(env, 'k1', { caps: ['seed'] }).verdict,
|
|
'deny'
|
|
)
|
|
t.is(
|
|
evaluateBareOsPeerAdmission(env, 'k1', { caps: ['seed', 'read'] })
|
|
.verdict,
|
|
'allow'
|
|
)
|
|
const denied = evaluateBareOsPeerAdmission(env, 'k1', {
|
|
caps: ['seed'],
|
|
dhtAddressClass: 'relay'
|
|
})
|
|
t.is(denied.reason, 'peer_missing_cap')
|
|
t.alike(denied.requiredCaps, ['seed', 'read'])
|
|
})
|
|
|
|
test('evaluateBareOsPeerAdmission denylist overrides satisfied require_caps', async (t) => {
|
|
const env = {
|
|
BARE_OS_PEER_DENYLIST_HEX: 'dead',
|
|
BARE_OS_PEER_REQUIRE_CAPS_JSON: '["seed"]'
|
|
}
|
|
const r = evaluateBareOsPeerAdmission(env, 'dead', { caps: ['seed'] })
|
|
t.is(r.verdict, 'deny')
|
|
t.is(r.reason, 'peer_denylist')
|
|
})
|
|
|
|
test('bareOsFormatPeerAdmissionAuditEvent never embeds full peer keys', (t) => {
|
|
const full = 'a'.repeat(64)
|
|
const res = evaluateBareOsPeerAdmission({}, full)
|
|
const ev = bareOsFormatPeerAdmissionAuditEvent(full, res, {
|
|
dhtAddressClass: 'ipv4'
|
|
})
|
|
t.is(ev.peerKeyHexPrefix, 'a'.repeat(16))
|
|
t.absent(ev.peerKeyHex)
|
|
t.is(ev.type, 'peer_admission')
|
|
t.is(ev.dhtAddressClass, 'ipv4')
|
|
})
|
|
|
|
test('bareOsPeerAdmissionAuditRateAllow throttles per bucket', (t) => {
|
|
const m = new Map()
|
|
t.ok(bareOsPeerAdmissionAuditRateAllow('b', 1000, 100, m))
|
|
t.absent(bareOsPeerAdmissionAuditRateAllow('b', 1050, 100, m))
|
|
t.ok(bareOsPeerAdmissionAuditRateAllow('b', 1100, 100, m))
|
|
})
|
|
|
|
test('bareOsDrainFcntlLockWaiters wakes FIFO waiters per path', (t) => {
|
|
const lockPath = '/coop/lockfifo'
|
|
const slot = { readers: new Set(), writer: 'holder1' }
|
|
const c = {
|
|
bareOsCooperativeLocks: { [lockPath]: slot },
|
|
bareOsFcntlLockWaitQueues: { [lockPath]: [] }
|
|
}
|
|
const q = c.bareOsFcntlLockWaitQueues[lockPath]
|
|
const order = []
|
|
q.push({
|
|
resolve: () => {
|
|
order.push('w2')
|
|
},
|
|
timer: null,
|
|
owner: 'sess2',
|
|
wantWrite: true
|
|
})
|
|
q.push({
|
|
resolve: () => {
|
|
order.push('w3')
|
|
},
|
|
timer: null,
|
|
owner: 'sess3',
|
|
wantWrite: true
|
|
})
|
|
slot.writer = null
|
|
bareOsDrainFcntlLockWaiters(c, lockPath)
|
|
t.alike(order, ['w2'])
|
|
t.is(slot.writer, 'sess2')
|
|
slot.writer = null
|
|
bareOsDrainFcntlLockWaiters(c, lockPath)
|
|
t.alike(order, ['w2', 'w3'])
|
|
t.is(slot.writer, 'sess3')
|
|
})
|
|
|
|
test('BARE_OS_CTX_API_VERSION is semver-shaped', async (t) => {
|
|
t.ok(/^\d+\.\d+\.\d+$/.test(BARE_OS_CTX_API_VERSION))
|
|
})
|
|
|
|
test('getBareOsPipelineLimits reads BARE_OS_PIPELINE_* from env', async (t) => {
|
|
const def = getBareOsPipelineLimits({})
|
|
t.is(def.maxStages, DEFAULT_PIPELINE_MAX_STAGES)
|
|
const custom = getBareOsPipelineLimits({
|
|
BARE_OS_PIPELINE_MAX_STAGES: '4',
|
|
BARE_OS_PIPELINE_MAX_BYTES: '100',
|
|
BARE_OS_PIPELINE_MAX_LINES: '20'
|
|
})
|
|
t.is(custom.maxStages, 4)
|
|
t.is(custom.maxBytes, 100)
|
|
t.is(custom.maxLines, 20)
|
|
})
|
|
|
|
test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t) => {
|
|
const caps = buildBareOsRuntimeCaps({
|
|
BARE_OS_PIPELINE_MAX_STAGES: '8',
|
|
BARE_OS_CTX_API_VERSION
|
|
})
|
|
t.is(caps.ctxApiVersion, BARE_OS_CTX_API_VERSION)
|
|
t.is(caps.pipeline.maxStages, 8)
|
|
t.ok(Array.isArray(caps.pseudoFsPaths))
|
|
t.ok(caps.pseudoFsPaths.includes('/proc/version'))
|
|
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/boot_profile'))
|
|
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/ipc'))
|
|
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/session'))
|
|
t.ok(caps.pseudoFsPaths.includes('/proc/mounts'))
|
|
t.ok(caps.pseudoFsPaths.includes('/proc/bare_os_resources'))
|
|
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/virtual'))
|
|
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/unit-journal'))
|
|
t.ok(caps.pseudoFsPaths.includes('/snapshots'))
|
|
t.ok(caps.pseudoFsPaths.includes('/snapshots/system'))
|
|
t.is(caps.features.simulatedPipelines, true)
|
|
t.is(caps.features.httpDelegate, true)
|
|
t.is(caps.features.gitDelegate, true)
|
|
t.is(caps.features.systemctlDelegate, true)
|
|
t.is(caps.features.bareCtxModules, true)
|
|
t.is(caps.features.bareDriveBundles, true)
|
|
t.is(caps.features.bareHostImportsForCtx, true)
|
|
const capsOff = buildBareOsRuntimeCaps({ BARE_OS_BARE_MODULES: '0' })
|
|
t.is(capsOff.features.bareCtxModules, false)
|
|
t.is(capsOff.features.bareDriveBundles, false)
|
|
t.is(capsOff.features.bareHostImportsForCtx, false)
|
|
const capsIso = buildBareOsRuntimeCaps({ BARE_OS_BARE_HOST_IMPORTS: '0' })
|
|
t.is(capsIso.features.bareHostImportsForCtx, false)
|
|
t.is(capsIso.features.bareCtxModules, true)
|
|
})
|
|
|
|
test('bareOsBareModulesEnabled respects BARE_OS_BARE_MODULES', async (t) => {
|
|
t.ok(bareOsBareModulesEnabled({}))
|
|
t.ok(!bareOsBareModulesEnabled({ BARE_OS_BARE_MODULES: '0' }))
|
|
t.ok(!bareOsBareModulesEnabled({ BARE_OS_BARE_MODULES: 'false' }))
|
|
})
|
|
|
|
test('loadBareModuleManifest has entries', async (t) => {
|
|
const m = loadBareModuleManifest()
|
|
t.ok(m.version >= 1)
|
|
t.ok(Array.isArray(m.entries))
|
|
t.ok(m.entries.some((e) => e.ctxKey === 'b4a'))
|
|
})
|
|
|
|
function stableStringifyManifest(obj) {
|
|
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj)
|
|
if (Array.isArray(obj)) {
|
|
return '[' + obj.map((x) => stableStringifyManifest(x)).join(',') + ']'
|
|
}
|
|
const keys = Object.keys(obj).sort()
|
|
return (
|
|
'{' +
|
|
keys
|
|
.map((k) => JSON.stringify(k) + ':' + stableStringifyManifest(obj[k]))
|
|
.join(',') +
|
|
'}'
|
|
)
|
|
}
|
|
|
|
test('bare-module-manifest.data.mjs matches bare-module-manifest.json', async (t) => {
|
|
const dir = path.dirname(fileURLToPath(import.meta.url))
|
|
const jsonPath = path.join(dir, 'lib', 'ctx', 'bare-module-manifest.json')
|
|
const fromJson = JSON.parse(await readFile(jsonPath, 'utf8'))
|
|
const embedded = bareOsBareModuleManifestEmbeddedRef()
|
|
t.is(stableStringifyManifest(fromJson), stableStringifyManifest(embedded))
|
|
})
|
|
|
|
test('buildBareCtxObjectFromHost loads core keys on Node', async (t) => {
|
|
const target = {}
|
|
await buildBareCtxObjectFromHost(
|
|
{
|
|
// Narrow host import set: parallel import of the full manifest is slow and
|
|
// some Bare-oriented packages disturb brittle's hrtime-based timers.
|
|
BARE_OS_BARE_HOST_ONLY_CTX_KEYS: 'b4a,compactEncoding,protomux'
|
|
},
|
|
target
|
|
)
|
|
t.ok(target.b4a)
|
|
t.ok(target.compactEncoding)
|
|
t.ok(target.protomux)
|
|
})
|
|
|
|
test('buildPearCtxObjectFromHost loads pear tier keys on Node', async (t) => {
|
|
const target = {}
|
|
await buildPearCtxObjectFromHost({}, target)
|
|
t.ok(target.pearBuild, 'pearBuild')
|
|
t.ok(target.bareBundleCompile, 'bareBundleCompile')
|
|
t.ok(target.bareBundleEvaluate, 'bareBundleEvaluate')
|
|
// pear-bundle and pear-ref are Pear-runtime-only; host stub skips them on Node.
|
|
})
|
|
|
|
test('maybeMergeBareFromDrive fills missing keys from bundle (mock vfs)', async (t) => {
|
|
const repoRoot = path.join(
|
|
fileURLToPath(new URL('.', import.meta.url)),
|
|
'..',
|
|
'..'
|
|
)
|
|
const bundleAbs = path.join(repoRoot, 'kernel/lib/bare/bundles/b4a.js')
|
|
const bundleSrc = await readFile(bundleAbs)
|
|
const manifest = {
|
|
version: 1,
|
|
bundles: [{ path: '/lib/bare/bundles/b4a.js', keys: ['b4a'] }]
|
|
}
|
|
const target = {}
|
|
const vfs = {
|
|
async readFile(p) {
|
|
if (p === '/lib/bare/manifest.json')
|
|
return b4a.from(JSON.stringify(manifest))
|
|
if (p === '/lib/bare/bundles/b4a.js') return new Uint8Array(bundleSrc)
|
|
return null
|
|
}
|
|
}
|
|
await maybeMergeBareFromDrive({}, vfs, target)
|
|
t.ok(target.b4a)
|
|
})
|
|
|
|
test('expandArgvAliases expands first word and keeps trailing argv', async (t) => {
|
|
t.alike(expandArgvAliases(['ll', 'z'], defaultShellAliases()), [
|
|
'ls',
|
|
'-la',
|
|
'z'
|
|
])
|
|
})
|
|
|
|
test('expandArgvAliases leaves sed unchanged', async (t) => {
|
|
t.alike(
|
|
expandArgvAliases(['sed', 's/a/b/', 'x.txt'], defaultShellAliases()),
|
|
['sed', 's/a/b/', 'x.txt']
|
|
)
|
|
})
|
|
|
|
test('defaultShellAliases does not remap sed', async (t) => {
|
|
t.is(defaultShellAliases().sed, undefined)
|
|
})
|
|
|
|
test('defaultShellAliases top maps to baretop', async (t) => {
|
|
t.is(defaultShellAliases().top, 'baretop')
|
|
})
|
|
|
|
test('defaultShellAliases btop maps to baretop', async (t) => {
|
|
t.is(defaultShellAliases().btop, 'baretop')
|
|
})
|
|
|
|
test('defaultShellAliases nano maps to edit', async (t) => {
|
|
t.is(defaultShellAliases().nano, 'edit')
|
|
t.alike(expandArgvAliases(['nano', 'x'], defaultShellAliases()), [
|
|
'edit',
|
|
'x'
|
|
])
|
|
})
|
|
|
|
test('expandArgvAliases throws on cyclic alias chain', async (t) => {
|
|
const cyclic = { a: 'b', b: 'a' }
|
|
t.exception(
|
|
() => expandArgvAliases(['a'], cyclic),
|
|
/alias: expansion nested too deeply/
|
|
)
|
|
})
|
|
|
|
test('loadBarerc applies export and alias from personal ~/.barerc', async (t) => {
|
|
const dir = testCorestoreDir('barerc')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('brc'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await personal.put(
|
|
personalHomeBacking('/home/user', '.barerc'),
|
|
b4a.from('export MYRC=1\nalias dog=echo woof\n')
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
await loadBarerc(ctx)
|
|
t.is(ctx.vfs.env.MYRC, '1')
|
|
t.is(ctx.shellAliases.dog, 'echo woof')
|
|
t.is(ctx.shellAliases.ll, 'ls -la')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t) => {
|
|
const dir = testCorestoreDir('barercskel')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('brcsk'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(drive, personal)
|
|
await loadBarerc(ctx, { createSkeletonIfMissing: true })
|
|
const back = await ctx.vfs.readFile('~/.barerc')
|
|
t.ok(back)
|
|
t.is(ctx.b4a.toString(back), BARERC_SKELETON)
|
|
t.is(ctx.shellAliases.ll, 'ls -la')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('loadBarerc theme nord sets REPL color env and LS_COLORS', async (t) => {
|
|
const dir = testCorestoreDir('barerctheme')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('brcth'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await personal.put(
|
|
personalHomeBacking('/home/user', '.barerc'),
|
|
b4a.from('theme nord\n')
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
await loadBarerc(ctx)
|
|
t.is(ctx.vfs.env.BARE_OS_THEME, 'nord')
|
|
t.ok(
|
|
String(ctx.vfs.env.BARE_OS_COLOR_PROMPT || '').includes('38;2;'),
|
|
'nord preset uses truecolor prompt'
|
|
)
|
|
t.ok(
|
|
String(ctx.vfs.env.LS_COLORS || '').includes('di='),
|
|
'LS_COLORS set from preset'
|
|
)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine barerc reload reapplies barerc', async (t) => {
|
|
const dir = testCorestoreDir('barercrel')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('brcrel'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await personal.put(
|
|
personalHomeBacking('/home/user', '.barerc'),
|
|
b4a.from('export MARK=before\n')
|
|
)
|
|
const ctx = testCtx(drive, personal)
|
|
await loadBarerc(ctx)
|
|
t.is(ctx.vfs.env.MARK, 'before')
|
|
await personal.put(
|
|
personalHomeBacking('/home/user', '.barerc'),
|
|
b4a.from('export MARK=after\n')
|
|
)
|
|
await execShellLine(ctx, 'barerc reload')
|
|
t.is(ctx.vfs.env.MARK, 'after')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('bareParseLsColors roundtrip', async (t) => {
|
|
const s = 'di=01;34:ln=36:ex=32:*.tar=01;31'
|
|
const m = bareParseLsColors(s)
|
|
t.is(m.di, '01;34')
|
|
t.is(m.ln, '36')
|
|
t.alike(bareParseLsColors(bareSerializeLsColors(m)), m)
|
|
})
|
|
|
|
test('bareLsColorOpenSgrFromMap directory and glob', async (t) => {
|
|
const map = bareParseLsColors('di=01;34:*.md=00;32')
|
|
const d = bareLsColorOpenSgrFromMap(
|
|
{ type: 'directory', mode: 0o040755 },
|
|
'foo',
|
|
map
|
|
)
|
|
t.ok(d.startsWith('\x1b['), 'directory colored')
|
|
const md = bareLsColorOpenSgrFromMap(
|
|
{ type: 'file', mode: 0o100644 },
|
|
'README.md',
|
|
map
|
|
)
|
|
t.ok(md.includes('32'), 'markdown glob')
|
|
})
|
|
|
|
test('bareLsColorOpenSgrFromMap mh for multi-link regular file', async (t) => {
|
|
const map = bareParseLsColors('mh=01;44:fi=40;31')
|
|
const one = bareLsColorOpenSgrFromMap(
|
|
{ type: 'file', mode: 0o100644, nlink: 1 },
|
|
'a',
|
|
map
|
|
)
|
|
const two = bareLsColorOpenSgrFromMap(
|
|
{ type: 'file', mode: 0o100644, nlink: 2 },
|
|
'a',
|
|
map
|
|
)
|
|
t.ok(two.includes('44'), 'nlink>1 uses mh SGR')
|
|
t.ok(one.includes('31'), 'nlink 1 uses fi')
|
|
})
|
|
|
|
test('bareLsColorOpenSgrFromMap ca when stat has capabilities', async (t) => {
|
|
const map = bareParseLsColors('ca=30;41:fi=00')
|
|
const sgr = bareLsColorOpenSgrFromMap(
|
|
{ type: 'file', mode: 0o100644, nlink: 1, capabilities: true },
|
|
'cap',
|
|
map
|
|
)
|
|
t.ok(sgr.includes('41'), 'capabilities use ca')
|
|
})
|
|
|
|
test('applyBareOsThemeFromEnv BARE_OS_COLOR_DEPTH=256 drops truecolor', async (t) => {
|
|
const ctx = {
|
|
vfs: { env: { BARE_OS_THEME: 'nord', BARE_OS_COLOR_DEPTH: '256' } }
|
|
}
|
|
await applyBareOsThemeFromEnv(ctx)
|
|
const p = String(ctx.vfs.env.BARE_OS_COLOR_PROMPT || '')
|
|
t.ok(p.includes('38;5;'), '256-color palette index SGR')
|
|
t.ok(!p.includes('38;2;'), 'no RGB truecolor')
|
|
})
|
|
|
|
test('dircolors -p includes TERM and di', async (t) => {
|
|
const db = bareDefaultDircolorsDatabase()
|
|
t.ok(db.includes('TERM'))
|
|
t.ok(db.includes('di '))
|
|
})
|
|
|
|
test('bareParseDircolorsDatabase TERM block', async (t) => {
|
|
const text = 'TERM xterm\ndi 01;34\nTERM none\nfi 00\nTERM *\nln 01;36\n'
|
|
const m = bareParseDircolorsDatabase(text, 'xterm')
|
|
t.is(m.di, '01;34')
|
|
t.is(m.ln, '01;36')
|
|
})
|
|
|
|
test('runBinCommand theme set writes barerc and updates env', async (t) => {
|
|
const themePath = path.join(__dirname, '../../kernel/bin/theme')
|
|
const themeSrc = await readFile(themePath, 'utf8')
|
|
const dir = testCorestoreDir('themecmd')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('thcmd'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/theme', b4a.from(themeSrc))
|
|
const ctx = testCtx(drive, personal)
|
|
await loadBarerc(ctx)
|
|
await runBinCommand(ctx, ['theme', 'set', 'dracula'])
|
|
t.is(ctx.vfs.env.BARE_OS_THEME, 'dracula')
|
|
const barc = ctx.b4a.toString(await ctx.vfs.readFile('~/.barerc'))
|
|
t.ok(barc.includes('theme dracula'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand dircolors -p prints database', async (t) => {
|
|
const dcPath = path.join(__dirname, '../../kernel/bin/dircolors')
|
|
const dcSrc = await readFile(dcPath, 'utf8')
|
|
const dir = testCorestoreDir('dircolp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('dcp'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/dircolors', b4a.from(dcSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['dircolors', '-p'])
|
|
const out = lines.join('\n')
|
|
t.ok(out.includes('TERM'))
|
|
t.ok(out.includes('di'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand edit --help prints usage', async (t) => {
|
|
const editPath = path.join(__dirname, '../../kernel/bin/edit')
|
|
const editSrc = await readFile(editPath, 'utf8')
|
|
const dir = testCorestoreDir('edithlp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('edh'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/edit', b4a.from(editSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['edit', '--help'])
|
|
const out = lines.join('\n')
|
|
t.ok(out.includes('usage:'))
|
|
t.ok(out.includes('TTY'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand edit requires TTY', async (t) => {
|
|
const editPath = path.join(__dirname, '../../kernel/bin/edit')
|
|
const editSrc = await readFile(editPath, 'utf8')
|
|
const dir = testCorestoreDir('ednotty')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('ednt'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/edit', b4a.from(editSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.replStdin = { isTTY: false }
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['edit', 'x.txt'])
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(lines.some((l) => l.includes('TTY')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand baretop --help prints usage', async (t) => {
|
|
const binPath = path.join(__dirname, '../../kernel/bin/baretop')
|
|
const src = await readFile(binPath, 'utf8')
|
|
const dir = testCorestoreDir('baretophlp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('bth'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/baretop', b4a.from(src))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['baretop', '--help'])
|
|
const out = lines.join('\n')
|
|
t.ok(out.includes('usage:'))
|
|
t.ok(out.includes('TTY'))
|
|
t.ok(out.toLowerCase().includes('dashboard'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand baretop requires TTY', async (t) => {
|
|
const binPath = path.join(__dirname, '../../kernel/bin/baretop')
|
|
const src = await readFile(binPath, 'utf8')
|
|
const dir = testCorestoreDir('baretopntty')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('btnt'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/baretop', b4a.from(src))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.replStdin = { isTTY: false }
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['baretop'])
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(lines.some((l) => l.includes('TTY')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('runBinCommand nano --help matches edit bundle', async (t) => {
|
|
const nanoPath = path.join(__dirname, '../../kernel/bin/nano')
|
|
const nanoSrc = await readFile(nanoPath, 'utf8')
|
|
const dir = testCorestoreDir('nanohlp')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('nanh'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/nano', b4a.from(nanoSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await runBinCommand(ctx, ['nano', '-h'])
|
|
const out = lines.join('\n')
|
|
t.ok(out.includes('usage:'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('tokenize leaves echo 2 > file as stdout redirect not 2>', async (t) => {
|
|
const toks = tokenize('echo 2 > /tmp/x')
|
|
const words = toks.filter((x) => x.type === 'word').map((x) => x.value)
|
|
t.ok(words.includes('2'))
|
|
t.ok(toks.some((x) => x.type === 'op' && x.value === '>'))
|
|
t.ok(!toks.some((x) => x.type === 'op' && x.value === '2>'))
|
|
})
|
|
|
|
test('execShellLine stderr 2> and 2>&1 in pipeline', async (t) => {
|
|
const dir = testCorestoreDir('sherr')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('serr'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const emit = `
|
|
async function run(ctx) {
|
|
ctx.console.log('OUT')
|
|
ctx.console.error('ERR')
|
|
}
|
|
`
|
|
const cat = `
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
async function run(ctx) {
|
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
|
}
|
|
`
|
|
await drive.put('/bin/emit', b4a.from(emit))
|
|
await drive.put('/bin/cat', b4a.from(cat))
|
|
const ctx = testCtx(drive, personal)
|
|
await execShellLine(ctx, 'emit 2>~/e.out')
|
|
const eb = await ctx.vfs.readFile('~/e.out')
|
|
t.ok(eb && ctx.b4a.toString(eb).includes('ERR'))
|
|
t.ok(!ctx.b4a.toString(eb).includes('OUT'))
|
|
await ctx.vfs.unlink('~/e.out')
|
|
const logs = []
|
|
ctx.console = {
|
|
log: (...a) => logs.push(['out', ...a]),
|
|
error: (...a) => logs.push(['err', ...a])
|
|
}
|
|
await execShellLine(ctx, 'emit 2>&1 | cat')
|
|
const piped = logs
|
|
.filter((x) => x[0] === 'out')
|
|
.map((x) => x.slice(1).join(' '))
|
|
t.ok(piped.some((l) => l.includes('OUT') && l.includes('ERR')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine pipeline < in and > out redirection', async (t) => {
|
|
const dir = testCorestoreDir('shpipeio')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('spio'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const cat = `
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
async function run(ctx) {
|
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
|
}
|
|
`
|
|
await drive.put('/bin/cat', b4a.from(cat))
|
|
const ctx = testCtx(drive, personal)
|
|
await ctx.vfs.writeFile('~/in.txt', b4a.from('pipeline-data\n'))
|
|
await execShellLine(ctx, 'cat < ~/in.txt | cat > ~/out.txt')
|
|
t.is(ctx.exitCode, 0)
|
|
t.is(
|
|
b4a.toString(await ctx.vfs.readFile('~/out.txt'), 'utf8'),
|
|
'pipeline-data\n'
|
|
)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine pipeline first stage stdin < file only', async (t) => {
|
|
const dir = testCorestoreDir('shpipein')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('spin'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const cat = `
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
async function run(ctx) {
|
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
|
}
|
|
`
|
|
const wcPath = path.join(__dirname, '../../kernel/bin/wc')
|
|
const wcSrc = await readFile(wcPath, 'utf8')
|
|
await drive.put('/bin/cat', b4a.from(cat))
|
|
await drive.put('/bin/wc', b4a.from(wcSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await ctx.vfs.writeFile('~/lines.txt', b4a.from('a\nb\nc\n'))
|
|
await execShellLine(ctx, 'wc -l < ~/lines.txt | cat')
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(lines.some((l) => /^\s*3\s/.test(l) || l.includes('3')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine pipeline stage and byte limits', async (t) => {
|
|
const dir = testCorestoreDir('shpipe')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('spipe'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const echo = `
|
|
async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`
|
|
const cat = `
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
async function run(ctx) {
|
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
|
}
|
|
`
|
|
await drive.put('/bin/echo', b4a.from(echo))
|
|
await drive.put('/bin/cat', b4a.from(cat))
|
|
const spam = `
|
|
async function run(ctx) {
|
|
ctx.console.log('z'.repeat(200))
|
|
}
|
|
`
|
|
await drive.put('/bin/spam', b4a.from(spam))
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '2'
|
|
await execShellLine(ctx, 'echo a | echo b | echo c')
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_STAGES')))
|
|
logs.length = 0
|
|
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '32'
|
|
ctx.env.BARE_OS_PIPELINE_MAX_BYTES = '80'
|
|
await execShellLine(ctx, 'spam | cat')
|
|
t.is(ctx.exitCode, 1)
|
|
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_BYTES')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine pipeline stage timeout aborts stalled stage safely', async (t) => {
|
|
const dir = testCorestoreDir('pipestagetimeout')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('ppipestagetimeout'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/hang',
|
|
b4a.from(
|
|
`async function run(){ await new Promise((resolve)=>setTimeout(resolve,200)); }`
|
|
)
|
|
)
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`async function run(ctx, argv){ ctx.console.log(argv.slice(1).join(' ')) }`)
|
|
)
|
|
const logs = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.vfs.env.BARE_OS_SHELL_PIPELINE_STAGE_TIMEOUT_MS = '20'
|
|
await execShellLine(ctx, 'hang | echo never')
|
|
t.ok(logs.some((l) => l.includes('pipeline stage timeout')))
|
|
t.is(ctx.exitCode, 1)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('ls bareOsStdoutCaptured lists one name per line', async (t) => {
|
|
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
|
|
const lsSrc = await readFile(lsPath, 'utf8')
|
|
const dir = testCorestoreDir('lscap')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('plcap'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/ls', b4a.from(lsSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await ctx.vfs.writeFile('aaa', b4a.from(''))
|
|
await ctx.vfs.writeFile('lib', b4a.from(''))
|
|
await ctx.vfs.writeFile('zzz', b4a.from(''))
|
|
await runBinCommand(Object.assign({}, ctx, { bareOsStdoutCaptured: true }), [
|
|
'ls'
|
|
])
|
|
t.is(lines.length, 3)
|
|
t.ok(lines.includes('aaa') && lines.includes('lib') && lines.includes('zzz'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine ls pipe grep prints only matching entry line', async (t) => {
|
|
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
|
|
const grepPath = path.join(__dirname, '../../kernel/bin/grep')
|
|
const lsSrc = await readFile(lsPath, 'utf8')
|
|
const grepSrc = await readFile(grepPath, 'utf8')
|
|
const dir = testCorestoreDir('lspipegrep')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('plpg'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/ls', b4a.from(lsSrc))
|
|
await drive.put('/bin/grep', b4a.from(grepSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await ctx.vfs.writeFile('aaa', b4a.from(''))
|
|
await ctx.vfs.writeFile('lib', b4a.from(''))
|
|
await ctx.vfs.writeFile('zzz', b4a.from(''))
|
|
await execShellLine(ctx, 'ls | grep -F lib')
|
|
t.is(ctx.exitCode, 0)
|
|
t.alike(lines, ['lib'])
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine ls pipe wc -l counts lines', async (t) => {
|
|
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
|
|
const wcPath = path.join(__dirname, '../../kernel/bin/wc')
|
|
const lsSrc = await readFile(lsPath, 'utf8')
|
|
const wcSrc = await readFile(wcPath, 'utf8')
|
|
const dir = testCorestoreDir('lspipewc')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('plwc'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put('/bin/ls', b4a.from(lsSrc))
|
|
await drive.put('/bin/wc', b4a.from(wcSrc))
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await ctx.vfs.writeFile('a', b4a.from(''))
|
|
await ctx.vfs.writeFile('b', b4a.from(''))
|
|
await ctx.vfs.writeFile('c', b4a.from(''))
|
|
await execShellLine(ctx, 'ls | wc -l')
|
|
t.is(ctx.exitCode, 0)
|
|
const out = lines.join('\n').trim()
|
|
t.is(out, '3', 'three directory entries => three lines')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine runs cd and external', async (t) => {
|
|
const dir = testCorestoreDir('sh')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('psh'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/xy',
|
|
b4a.from(`
|
|
async function run(ctx, argv) {
|
|
ctx.got.push(argv.join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const got = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.got = got
|
|
await execShellLine(ctx, 'cd /bin')
|
|
t.is(ctx.vfs.getcwd(), '/bin')
|
|
await execShellLine(ctx, 'xy one two')
|
|
t.is(got[0], 'xy one two')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine expands default ll to ls -la', async (t) => {
|
|
const dir = testCorestoreDir('llalias')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('lla'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/ls',
|
|
b4a.from(`
|
|
async function run(ctx, argv) {
|
|
ctx.got.push(argv.join(' '))
|
|
}
|
|
`)
|
|
)
|
|
const got = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.got = got
|
|
await execShellLine(ctx, 'll one')
|
|
t.is(got[0], 'ls -la one')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('tokenize && || ; and split helpers', async (t) => {
|
|
const toks = tokenize('a&&b||c;d')
|
|
t.is(toks.map((x) => x.value).join(' '), 'a && b || c ; d')
|
|
const lists = splitTokensBySemicolon(toks)
|
|
t.is(lists.length, 2)
|
|
const { segments, ops } = splitTokensByAndOr(lists[0])
|
|
t.is(segments.length, 3)
|
|
t.is(ops.join(' '), '&& ||')
|
|
})
|
|
|
|
test('execShellLine && || ; short-circuit and exitCode', async (t) => {
|
|
const dir = testCorestoreDir('shandor')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sand'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
const stub = `
|
|
async function run(ctx, argv) {
|
|
const c = argv[0]
|
|
ctx.ran.push(c)
|
|
ctx.exitCode = c === 'false' ? 1 : 0
|
|
}
|
|
`
|
|
await drive.put('/bin/true', b4a.from(stub))
|
|
await drive.put('/bin/false', b4a.from(stub))
|
|
await drive.put(
|
|
'/bin/rec',
|
|
b4a.from(`
|
|
async function run(ctx, argv) {
|
|
ctx.ran.push('rec')
|
|
ctx.exitCode = 0
|
|
}
|
|
`)
|
|
)
|
|
const ran = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.ran = ran
|
|
ctx.exitCode = 0
|
|
|
|
await execShellLine(ctx, 'false && rec')
|
|
t.is(ran.join(','), 'false')
|
|
t.is(ctx.exitCode, 1)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'false || rec')
|
|
t.is(ran.join(','), 'false,rec')
|
|
t.is(ctx.exitCode, 0)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'true && rec')
|
|
t.is(ran.join(','), 'true,rec')
|
|
t.is(ctx.exitCode, 0)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'true || rec')
|
|
t.is(ran.join(','), 'true')
|
|
t.is(ctx.exitCode, 0)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'false || false || rec')
|
|
t.is(ran.join(','), 'false,false,rec')
|
|
t.is(ctx.exitCode, 0)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'true && false || rec')
|
|
t.is(ran.join(','), 'true,false,rec')
|
|
t.is(ctx.exitCode, 0)
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'true; rec')
|
|
t.is(ran.join(','), 'true,rec')
|
|
|
|
await drive.put(
|
|
'/bin/last',
|
|
b4a.from(`
|
|
async function run(ctx, argv) {
|
|
ctx.ran.push('last')
|
|
ctx.exitCode = 0
|
|
}
|
|
`)
|
|
)
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'false | last')
|
|
t.is(ran.join(','), 'false,last', 'pipeline runs both stages')
|
|
t.is(ctx.exitCode, 0, 'POSIX-like: pipeline exit status is last stage')
|
|
|
|
ran.length = 0
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'BARE_OS_SHELL_PIPEFAIL=1 false | last')
|
|
t.is(ran.join(','), 'false,last')
|
|
t.is(
|
|
ctx.exitCode,
|
|
1,
|
|
'pipefail: failure in an earlier stage overrides last stage success'
|
|
)
|
|
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('execShellLine if then else fi', async (t) => {
|
|
const dir = testCorestoreDir('shif')
|
|
const store = new Corestore(dir)
|
|
const drive = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('sif'))
|
|
await drive.ready()
|
|
await personal.ready()
|
|
await drive.put(
|
|
'/bin/echo',
|
|
b4a.from(`
|
|
async function run(ctx, argv) {
|
|
ctx.console.log(argv.slice(1).join(' '))
|
|
}
|
|
`)
|
|
)
|
|
await drive.put(
|
|
'/bin/true',
|
|
b4a.from(`
|
|
async function run(ctx) {
|
|
ctx.exitCode = 0
|
|
}
|
|
`)
|
|
)
|
|
await drive.put(
|
|
'/bin/false',
|
|
b4a.from(`
|
|
async function run(ctx) {
|
|
ctx.exitCode = 1
|
|
}
|
|
`)
|
|
)
|
|
const lines = []
|
|
const ctx = testCtx(drive, personal)
|
|
ctx.console = {
|
|
log: (s) => lines.push(String(s)),
|
|
error: (...a) => lines.push(a.join(' '))
|
|
}
|
|
await execShellLine(ctx, 'if false; then echo no; else echo yes; fi')
|
|
t.ok(lines.some((l) => l.includes('yes')))
|
|
lines.length = 0
|
|
await execShellLine(ctx, 'if true; then echo ok; fi')
|
|
t.ok(lines.some((l) => l.includes('ok')))
|
|
lines.length = 0
|
|
await execShellLine(ctx, 'if false; then echo x; fi')
|
|
t.ok(!lines.some((l) => l.includes('x')))
|
|
ctx.exitCode = 0
|
|
await execShellLine(ctx, 'n=0; if true; then n=$((n+1)); fi; echo $n')
|
|
t.is(ctx.exitCode, 0)
|
|
t.ok(lines.some((l) => l.includes('1')))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('filenameFromContentDisposition parses attachment names', async (t) => {
|
|
t.is(
|
|
filenameFromContentDisposition('attachment; filename="a b.txt"'),
|
|
'a b.txt'
|
|
)
|
|
t.is(
|
|
filenameFromContentDisposition("attachment; filename*=UTF-8''x%20y.bin"),
|
|
'x y.bin'
|
|
)
|
|
t.is(filenameFromContentDisposition(null), null)
|
|
})
|
|
|
|
test('cron fieldMatches and dowFieldMatches', async (t) => {
|
|
t.ok(fieldMatches('*', 0, 0, 59))
|
|
t.ok(fieldMatches('*/5', 10, 0, 59))
|
|
t.ok(!fieldMatches('*/5', 11, 0, 59))
|
|
t.ok(fieldMatches('1-3', 2, 0, 59))
|
|
t.ok(!fieldMatches('1-3', 4, 0, 59))
|
|
t.ok(fieldMatches('1,4', 1, 0, 59))
|
|
t.ok(fieldMatches('1,4', 4, 0, 59))
|
|
t.ok(fieldMatches('1-10/2', 3, 0, 59))
|
|
t.ok(!fieldMatches('1-10/2', 4, 0, 59))
|
|
t.ok(dowFieldMatches('7', 0))
|
|
t.ok(!dowFieldMatches('7', 1))
|
|
t.ok(dowFieldMatches('0', 0))
|
|
t.ok(dowFieldMatches('1-5', 3))
|
|
})
|
|
|
|
test('cron parseCronLine and jobMatchesDate', async (t) => {
|
|
t.absent(parseCronLine(''))
|
|
t.absent(parseCronLine('# comment'))
|
|
t.absent(parseCronLine('0 0 * *'))
|
|
const j = parseCronLine('30 14 15 6 * echo hello world')
|
|
t.ok(j)
|
|
t.is(j.command, 'echo hello world')
|
|
const when = new Date(2020, 5, 15, 14, 30, 0)
|
|
t.ok(jobMatchesDate(j, when))
|
|
t.ok(!jobMatchesDate(j, new Date(2020, 5, 15, 14, 31, 0)))
|
|
const orJob = parseCronLine('0 12 15 * 1 echo dom-or-mon')
|
|
t.ok(orJob)
|
|
t.ok(
|
|
jobMatchesDate(orJob, new Date(2020, 5, 15, 12, 0, 0)),
|
|
'15th matches when dow also set (OR)'
|
|
)
|
|
t.ok(
|
|
jobMatchesDate(orJob, new Date(2020, 5, 8, 12, 0, 0)),
|
|
'Monday matches when dom also set (OR)'
|
|
)
|
|
t.ok(!jobMatchesDate(orJob, new Date(2020, 5, 16, 12, 0, 0)), 'Tue 16th no match')
|
|
const reboot = parseCronLine('@reboot echo hi')
|
|
t.ok(reboot)
|
|
t.is(reboot.minute, '@reboot')
|
|
t.is(reboot.command, 'echo hi')
|
|
t.ok(!jobMatchesDate(reboot, when))
|
|
const jit = parseCronLine('@reboot JitterSec=12 /bin/true')
|
|
t.ok(jit)
|
|
t.is(jit.jitterSec, 12)
|
|
t.is(jit.command, '/bin/true')
|
|
const jl = parseCronLine('* * * * * JitterSec=3 echo x')
|
|
t.ok(jl)
|
|
t.is(jl.jitterSec, 3)
|
|
t.is(jl.command, 'echo x')
|
|
})
|
|
|
|
test('systemctl list after startBareInitd shows kernel-logger and bare-cron', async (t) => {
|
|
const dir = testCorestoreDir('initctl')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pinit'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const logs = []
|
|
const ctx = testCtx(sys, personal)
|
|
ctx.execLine = async () => {}
|
|
await startBareInitd(ctx)
|
|
const dagRaw = getLastBareInitdDagSnapshotJson()
|
|
const dag = JSON.parse(String(dagRaw).trim())
|
|
t.ok(dag && dag.supervision && dag.supervision.schema === 1)
|
|
t.ok(Array.isArray(dag.supervision.restartKeys))
|
|
ctx.console = {
|
|
log: (...a) => logs.push(a.join(' ')),
|
|
error: (...a) => logs.push(a.join(' '))
|
|
}
|
|
ctx.exitCode = 0
|
|
await runBinCommand(ctx, ['systemctl', 'list'])
|
|
t.is(ctx.exitCode, 0)
|
|
const text = logs.join('\n')
|
|
t.ok(text.includes('kernel-logger'))
|
|
t.ok(text.includes('bare-cron'))
|
|
t.ok(text.includes('bare-openssh'))
|
|
stopBareInitd()
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|