Stabilize paired reviewer recovery and owner follow-up

This commit is contained in:
ejclaw
2026-04-06 07:49:26 +09:00
parent 994e957767
commit d75d26c23d
27 changed files with 1474 additions and 166 deletions

View File

@@ -1,6 +1,9 @@
import { describe, it, expect } from 'vitest';
import {
ensureLinuxReadonlySandboxAppArmorSupport,
getAppArmorRestrictUnprivilegedUsernsValue,
canUseLinuxBubblewrapReadonlySandbox,
getPlatform,
isWSL,
isRoot,
@@ -99,6 +102,218 @@ describe('commandExists', () => {
});
});
describe('canUseLinuxBubblewrapReadonlySandbox', () => {
it('returns false outside linux', () => {
expect(
canUseLinuxBubblewrapReadonlySandbox(
'macos',
() => {
throw new Error('should not probe commands on macOS');
},
),
).toBe(false);
});
it('returns false when bwrap is not installed', () => {
expect(
canUseLinuxBubblewrapReadonlySandbox(
'linux',
() => false,
),
).toBe(false);
});
it('returns true when linux bwrap readonly probe succeeds', () => {
expect(
canUseLinuxBubblewrapReadonlySandbox(
'linux',
() => true,
(() => Buffer.from('')) as typeof import('child_process').execSync,
),
).toBe(true);
});
it('returns false when linux bwrap readonly probe fails', () => {
expect(
canUseLinuxBubblewrapReadonlySandbox(
'linux',
() => true,
(() => {
throw new Error('permission denied');
}) as typeof import('child_process').execSync,
),
).toBe(false);
});
});
describe('getAppArmorRestrictUnprivilegedUsernsValue', () => {
it('returns null when the proc sysctl path is unavailable', () => {
expect(
getAppArmorRestrictUnprivilegedUsernsValue((() => {
throw new Error('missing');
}) as typeof import('fs').readFileSync),
).toBeNull();
});
it('returns the trimmed proc sysctl value', () => {
expect(
getAppArmorRestrictUnprivilegedUsernsValue((() =>
'1\n') as typeof import('fs').readFileSync),
).toBe('1');
});
});
describe('ensureLinuxReadonlySandboxAppArmorSupport', () => {
it('does nothing outside linux', () => {
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'macos',
}),
).toBe('not-linux');
});
it('does nothing when apparmor does not block sandboxing', () => {
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '0',
sandboxCapable: false,
}),
).toBe('not-needed');
});
it('does nothing when sandbox is already capable', () => {
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: true,
}),
).toBe('not-needed');
});
it('requires root when linux apparmor blocks sandboxing', () => {
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: false,
isRootUser: false,
}),
).toBe('requires-root');
});
it('writes and applies the sysctl when running as root', () => {
const writes: string[] = [];
const commands: string[] = [];
const fileContents = new Map<string, string>([
['/proc/sys/kernel/apparmor_restrict_unprivileged_userns', '1\n'],
]);
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: false,
isRootUser: true,
readFileSyncFn: ((target) => {
const value = fileContents.get(String(target));
if (value == null) throw new Error('missing');
return value;
}) as typeof import('fs').readFileSync,
mkdirSyncFn: (() => undefined) as typeof import('fs').mkdirSync,
writeFileSyncFn: ((target, contents) => {
const normalizedTarget = String(target);
const normalizedContents = String(contents);
writes.push(`${normalizedTarget}\n${normalizedContents}`);
fileContents.set(normalizedTarget, normalizedContents);
}) as typeof import('fs').writeFileSync,
execFn: ((command) => {
commands.push(String(command));
fileContents.set(
'/proc/sys/kernel/apparmor_restrict_unprivileged_userns',
'0\n',
);
return Buffer.from('');
}) as typeof import('child_process').execSync,
}),
).toBe('configured');
expect(writes[0]).toContain('/etc/sysctl.d/90-ejclaw-sandbox.conf');
expect(writes[0]).toContain(
'kernel.apparmor_restrict_unprivileged_userns=0',
);
expect(commands).toContain(
'sysctl -w kernel.apparmor_restrict_unprivileged_userns=0',
);
});
it('skips rewriting an already-correct sysctl file but still applies and verifies', () => {
const writes: string[] = [];
const commands: string[] = [];
const fileContents = new Map<string, string>([
['/etc/sysctl.d/90-ejclaw-sandbox.conf',
'# Managed by EJClaw setup to allow bubblewrap readonly sandboxing.\n' +
'kernel.apparmor_restrict_unprivileged_userns=0\n'],
['/proc/sys/kernel/apparmor_restrict_unprivileged_userns', '1\n'],
]);
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: false,
isRootUser: true,
readFileSyncFn: ((target) => {
const value = fileContents.get(String(target));
if (value == null) throw new Error('missing');
return value;
}) as typeof import('fs').readFileSync,
mkdirSyncFn: (() => undefined) as typeof import('fs').mkdirSync,
writeFileSyncFn: ((target, contents) => {
writes.push(`${String(target)}\n${String(contents)}`);
}) as typeof import('fs').writeFileSync,
execFn: ((command) => {
commands.push(String(command));
fileContents.set(
'/proc/sys/kernel/apparmor_restrict_unprivileged_userns',
'0\n',
);
return Buffer.from('');
}) as typeof import('child_process').execSync,
}),
).toBe('configured');
expect(writes).toHaveLength(0);
expect(commands).toContain(
'sysctl -w kernel.apparmor_restrict_unprivileged_userns=0',
);
});
it('returns failed when the sysctl apply does not take effect', () => {
const fileContents = new Map<string, string>([
['/proc/sys/kernel/apparmor_restrict_unprivileged_userns', '1\n'],
]);
expect(
ensureLinuxReadonlySandboxAppArmorSupport({
platform: 'linux',
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: false,
isRootUser: true,
readFileSyncFn: ((target) => {
const value = fileContents.get(String(target));
if (value == null) throw new Error('missing');
return value;
}) as typeof import('fs').readFileSync,
mkdirSyncFn: (() => undefined) as typeof import('fs').mkdirSync,
writeFileSyncFn: (() => undefined) as typeof import('fs').writeFileSync,
execFn: (() => Buffer.from('')) as typeof import('child_process').execSync,
}),
).toBe('failed');
});
});
// --- getNodeVersion ---
describe('getNodeVersion', () => {