build: unify bun quality gate

This commit is contained in:
ejclaw
2026-04-11 04:47:16 +09:00
parent f10833e818
commit 94d53e4cc3
29 changed files with 574 additions and 297 deletions

View File

@@ -55,7 +55,9 @@ export function normalizeLegacyRegisteredGroupsTable(database: Database): void {
const hasIsMain = legacyCols.some((col) => col.name === 'is_main');
const hasAgentType = legacyCols.some((col) => col.name === 'agent_type');
const hasWorkDir = legacyCols.some((col) => col.name === 'work_dir');
const hasAgentConfig = legacyCols.some((col) => col.name === 'agent_config');
const hasAgentConfig = legacyCols.some(
(col) => col.name === 'agent_config',
);
const hasContainerConfig = legacyCols.some(
(col) => col.name === 'container_config',
);

View File

@@ -65,7 +65,18 @@ function backupLegacyRows(database: Database): number {
requires_trigger, is_main, agent_type, work_dir
FROM registered_groups`,
)
.all() as Array<Record<string, unknown>>;
.all() as Array<{
jid: string;
name: string;
folder: string;
trigger_pattern: string;
added_at: string;
agent_config: string | null;
requires_trigger: number | null;
is_main: number | null;
agent_type: string | null;
work_dir: string | null;
}>;
const insert = database.prepare(
`INSERT OR REPLACE INTO registered_groups_legacy_backup (

View File

@@ -105,43 +105,30 @@ describe('commandExists', () => {
describe('canUseLinuxBubblewrapReadonlySandbox', () => {
it('returns false outside linux', () => {
expect(
canUseLinuxBubblewrapReadonlySandbox(
'macos',
() => {
throw new Error('should not probe commands on macOS');
},
),
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);
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,
),
canUseLinuxBubblewrapReadonlySandbox('linux', () => true, (() =>
Buffer.from('')) as unknown 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,
),
canUseLinuxBubblewrapReadonlySandbox('linux', () => true, (() => {
throw new Error('permission denied');
}) as typeof import('child_process').execSync),
).toBe(false);
});
});
@@ -157,8 +144,9 @@ describe('getAppArmorRestrictUnprivilegedUsernsValue', () => {
it('returns the trimmed proc sysctl value', () => {
expect(
getAppArmorRestrictUnprivilegedUsernsValue((() =>
'1\n') as typeof import('fs').readFileSync),
getAppArmorRestrictUnprivilegedUsernsValue(
(() => '1\n') as unknown as typeof import('fs').readFileSync,
),
).toBe('1');
});
});
@@ -252,9 +240,11 @@ describe('ensureLinuxReadonlySandboxAppArmorSupport', () => {
const writes: string[] = [];
const commands: string[] = [];
const fileContents = new Map<string, string>([
['/etc/sysctl.d/90-ejclaw-sandbox.conf',
[
'/etc/sysctl.d/90-ejclaw-sandbox.conf',
'# Managed by EJClaw setup to allow bubblewrap readonly sandboxing.\n' +
'kernel.apparmor_restrict_unprivileged_userns=0\n'],
'kernel.apparmor_restrict_unprivileged_userns=0\n',
],
['/proc/sys/kernel/apparmor_restrict_unprivileged_userns', '1\n'],
]);
@@ -301,14 +291,17 @@ describe('ensureLinuxReadonlySandboxAppArmorSupport', () => {
apparmorRestrictUnprivilegedUserns: '1',
sandboxCapable: false,
isRootUser: true,
readFileSyncFn: ((target) => {
readFileSyncFn: ((target: import('fs').PathOrFileDescriptor) => {
const value = fileContents.get(String(target));
if (value == null) throw new Error('missing');
return value;
}) as typeof import('fs').readFileSync,
}) as unknown 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,
execFn: (() =>
Buffer.from(
'',
)) as unknown as typeof import('child_process').execSync,
}),
).toBe('failed');
});

View File

@@ -177,10 +177,7 @@ export function ensureLinuxReadonlySandboxAppArmorSupport(options?: {
const sandboxCapable =
options?.sandboxCapable ?? canUseLinuxBubblewrapReadonlySandbox(platform);
if (
apparmorRestrictUnprivilegedUserns !== '1' ||
sandboxCapable
) {
if (apparmorRestrictUnprivilegedUserns !== '1' || sandboxCapable) {
return 'not-needed';
}
@@ -211,9 +208,8 @@ export function ensureLinuxReadonlySandboxAppArmorSupport(options?: {
stdio: 'ignore',
});
const appliedValue = getAppArmorRestrictUnprivilegedUsernsValue(
readFileSyncFn,
);
const appliedValue =
getAppArmorRestrictUnprivilegedUsernsValue(readFileSyncFn);
if (appliedValue !== '0') {
throw new Error(
`kernel.apparmor_restrict_unprivileged_userns remained ${appliedValue ?? 'unavailable'} after sysctl apply`,

View File

@@ -77,24 +77,24 @@ describe('restartStackServices', () => {
expect(() =>
restartStackServices(tempRoot, {
serviceManager: 'nohup',
serviceManager: 'none',
}),
).toThrow('restart:stack only supports Linux systemd services in this repo');
).toThrow(
'restart:stack only supports Linux systemd services in this repo',
);
});
it('falls back to direct restart when the oneshot unit is not installed for an external caller', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
tempRoots.push(tempRoot);
const execFileSyncImpl = vi
.fn()
.mockImplementationOnce(() => {
const error = new Error('unit missing');
Object.assign(error, {
stderr: 'Unit ejclaw-stack-restart.service not found.',
});
throw error;
const execFileSyncImpl = vi.fn().mockImplementationOnce(() => {
const error = new Error('unit missing');
Object.assign(error, {
stderr: 'Unit ejclaw-stack-restart.service not found.',
});
throw error;
});
const services = restartStackServices(tempRoot, {
execFileSyncImpl,
@@ -147,7 +147,8 @@ describe('restartStackServices', () => {
const execFileSyncImpl = vi.fn().mockImplementation(() => {
const error = new Error('job failed');
Object.assign(error, {
stderr: 'Job for ejclaw-stack-restart.service failed because the control process exited with error code.',
stderr:
'Job for ejclaw-stack-restart.service failed because the control process exited with error code.',
});
throw error;
});

View File

@@ -36,7 +36,7 @@ function restartStackServicesDirect(
}
const execImpl = deps.execFileSyncImpl ?? execFileSync;
const systemctlArgs = deps.runningAsRoot ?? isRoot() ? [] : ['--user'];
const systemctlArgs = (deps.runningAsRoot ?? isRoot()) ? [] : ['--user'];
execImpl('systemctl', [...systemctlArgs, 'restart', ...services], {
stdio: 'ignore',
@@ -64,7 +64,7 @@ export function restartStackServices(
const services = getConfiguredServiceNames(projectRoot);
const execImpl = deps.execFileSyncImpl ?? execFileSync;
const systemctlArgs = deps.runningAsRoot ?? isRoot() ? [] : ['--user'];
const systemctlArgs = (deps.runningAsRoot ?? isRoot()) ? [] : ['--user'];
if (deps.direct) {
return restartStackServicesDirect(projectRoot, deps);

View File

@@ -118,9 +118,7 @@ describe('systemd unit generation', () => {
'/home/user',
false,
);
expect(unit).toContain(
'ExecStart=/usr/bin/bun /srv/ejclaw/dist/index.js',
);
expect(unit).toContain('ExecStart=/usr/bin/bun /srv/ejclaw/dist/index.js');
});
it('preserves EnvironmentFile and extraEnv in the actual builder', () => {

View File

@@ -52,9 +52,12 @@ describe('verify service checks', () => {
execSyncMock.mockReturnValue(undefined);
expect(checkSystemdService('ejclaw')).toBe('running');
expect(execSyncMock).toHaveBeenCalledWith('systemctl --user is-active ejclaw', {
stdio: 'ignore',
});
expect(execSyncMock).toHaveBeenCalledWith(
'systemctl --user is-active ejclaw',
{
stdio: 'ignore',
},
);
});
it('checks systemd services in both explicit scopes', () => {
@@ -71,8 +74,12 @@ describe('verify service checks', () => {
throw new Error(`unexpected command: ${cmd}`);
});
expect(checkSystemdServiceInScope('ejclaw-codex', 'system')).toBe('running');
expect(checkSystemdServiceInScope('ejclaw-codex', 'user')).toBe('not_found');
expect(checkSystemdServiceInScope('ejclaw-codex', 'system')).toBe(
'running',
);
expect(checkSystemdServiceInScope('ejclaw-codex', 'user')).toBe(
'not_found',
);
});
it('treats an unloaded launchd plist as stopped when artifact detection is enabled', () => {
@@ -87,9 +94,9 @@ describe('verify service checks', () => {
fs.writeFileSync(plistPath, '<plist />');
execSyncMock.mockReturnValue('');
expect(
checkLaunchdServiceArtifact('com.ejclaw-codex', plistPath),
).toBe('stopped');
expect(checkLaunchdServiceArtifact('com.ejclaw-codex', plistPath)).toBe(
'stopped',
);
fs.rmSync(tempHome, { recursive: true, force: true });
});
@@ -111,7 +118,7 @@ describe('verify service checks', () => {
const killSpy = vi
.spyOn(process, 'kill')
.mockImplementation((_pid: number, _signal?: number | NodeJS.Signals) => true);
.mockImplementation((_pid: number, _signal?: string | number) => true);
expect(checkNohupService(tempRoot, 'ejclaw')).toBe('running');
@@ -121,7 +128,10 @@ describe('verify service checks', () => {
it('treats a legacy nohup wrapper without a live pid as stopped when artifact detection is enabled', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-'));
fs.writeFileSync(path.join(tempRoot, 'start-ejclaw-codex.sh'), '#!/bin/bash\n');
fs.writeFileSync(
path.join(tempRoot, 'start-ejclaw-codex.sh'),
'#!/bin/bash\n',
);
expect(checkNohupServiceArtifact(tempRoot, 'ejclaw-codex')).toBe('stopped');

View File

@@ -27,7 +27,9 @@ export function checkLaunchdService(label: string): ServiceStatus {
try {
const output = execSync('launchctl list', { encoding: 'utf-8' });
if (output.includes(label)) {
const line = output.split('\n').find((candidate) => candidate.includes(label));
const line = output
.split('\n')
.find((candidate) => candidate.includes(label));
if (line) {
const pidField = line.trim().split(/\s+/)[0];
return pidField !== '-' && pidField ? 'running' : 'stopped';