build: unify bun quality gate
This commit is contained in:
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user