221 lines
8.6 KiB
JavaScript
221 lines
8.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { classifyRuntimeImpact } from '../../scripts/runtime-impact.mjs';
|
|
|
|
const root = path.resolve(import.meta.dirname, '../..');
|
|
|
|
function git(cwd, ...args) {
|
|
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
}
|
|
|
|
test('runtime impact keeps affected components separate from restart scope', () => {
|
|
assert.deepEqual(classifyRuntimeImpact(['README.md', 'workpack/STATUS.md', 'test/example.test.js']), {
|
|
affectedComponents: [],
|
|
restartScope: 'none',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['scripts/clean-test-dist.mjs']), {
|
|
affectedComponents: [],
|
|
restartScope: 'none',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['.tmp-tests/harbor-route-test/network.json']), {
|
|
affectedComponents: [],
|
|
restartScope: 'none',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['context.md']), {
|
|
affectedComponents: [],
|
|
restartScope: 'none',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['src/web/App.tsx']), {
|
|
affectedComponents: ['control'],
|
|
restartScope: 'control',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['src/server/services/domainTrafficService.ts']), {
|
|
affectedComponents: ['dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['package-lock.json']), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
});
|
|
|
|
test('every displaced regex path keeps dataplane restart coverage in JS and TS', () => {
|
|
const legacyPaths = [
|
|
'src/server/config',
|
|
'src/server/dataplane',
|
|
'src/server/gatewayRouting',
|
|
'src/server/singbox',
|
|
'src/server/singboxRuntime',
|
|
'src/server/version',
|
|
'src/server/adapters/neighbors',
|
|
'src/server/services/connectivityDiagnosticsService',
|
|
'src/server/services/deviceTrafficService',
|
|
'src/server/services/devicePolicyService',
|
|
'src/server/services/singboxSelectorService',
|
|
'src/shared/connectivityDiagnostics',
|
|
'src/shared/errors',
|
|
];
|
|
for (const file of legacyPaths) {
|
|
assert.equal(classifyRuntimeImpact([`${file}.js`]).restartScope, 'both', `${file}.js`);
|
|
assert.equal(classifyRuntimeImpact([`${file}.ts`]).restartScope, 'both', `${file}.ts`);
|
|
}
|
|
for (const file of ['Dockerfile', 'entrypoint.sh', 'package.json', 'package-lock.json',
|
|
'scripts/build-runtime-base.sh', '.gitea/workflows/gateway-build.yml']) {
|
|
assert.equal(classifyRuntimeImpact([file]).restartScope, 'both', file);
|
|
}
|
|
});
|
|
|
|
test('classifier covers current dataplane reachability without promoting its client adapter', () => {
|
|
for (const file of ['src/server/main.js', 'src/server/main.ts']) {
|
|
assert.deepEqual(classifyRuntimeImpact([file]), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
}, file);
|
|
}
|
|
assert.deepEqual(classifyRuntimeImpact(['src/server/services/domainTrafficService.js']), {
|
|
affectedComponents: ['dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['src/server/services/deviceInventoryService.js']), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['src/server/services/singboxSelectorService.ts']), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
assert.deepEqual(classifyRuntimeImpact(['src/server/dataplaneClient.ts']), {
|
|
affectedComponents: ['control'],
|
|
restartScope: 'control',
|
|
});
|
|
});
|
|
|
|
test('native traffic contracts and collector restart both Gateway processes', () => {
|
|
for (const file of [
|
|
'buf.gen.yaml',
|
|
'proto/sing-box/v1.14.0-rc.5/daemon/started_service.proto',
|
|
'src/server/gatewayNativeRuntime.ts',
|
|
'src/server/generated/daemon/started_service_pb.ts',
|
|
'src/server/services/liveTrafficService.ts',
|
|
'src/shared/liveTraffic.ts',
|
|
'.node-version',
|
|
'scripts/check-sqlite-runtime.mjs',
|
|
'src/server/services/sqlite.ts',
|
|
'src/server/services/trafficHistoryStore.ts',
|
|
'src/server/services/trafficHistoryService.ts',
|
|
'src/server/services/trafficHistoryWorker.ts',
|
|
'src/shared/trafficHistory.ts',
|
|
]) {
|
|
assert.deepEqual(classifyRuntimeImpact([file]), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
}, file);
|
|
}
|
|
for (const file of [
|
|
'tools/test-singbox-client-rc.sh',
|
|
'tools/test-singbox-gateway-native-traffic.sh',
|
|
'tools/test-singbox-native-traffic.sh',
|
|
]) {
|
|
assert.deepEqual(classifyRuntimeImpact([file]), {
|
|
affectedComponents: [],
|
|
restartScope: 'none',
|
|
}, file);
|
|
}
|
|
});
|
|
|
|
test('version bumps do not promote a control-only change to a dataplane restart', () => {
|
|
assert.deepEqual(classifyRuntimeImpact(['src/web/App.tsx', 'src/shared/versions.ts']), {
|
|
affectedComponents: ['control'],
|
|
restartScope: 'control',
|
|
});
|
|
});
|
|
|
|
test('release-critical inputs restart both and unknown paths fail closed', () => {
|
|
for (const file of [
|
|
'.dockerignore',
|
|
'.gitea/workflows/another.yml',
|
|
'docker-compose.gateway.yml',
|
|
'scripts/deploy-gateway.sh',
|
|
]) {
|
|
assert.deepEqual(classifyRuntimeImpact([file]), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
}, file);
|
|
}
|
|
assert.throws(() => classifyRuntimeImpact(['src/new-owner.ts']), /Unclassified path/);
|
|
assert.throws(() => classifyRuntimeImpact(['scripts/new-runtime.sh']), /Unclassified path/);
|
|
});
|
|
|
|
test('every repository path has an explicit ownership class', () => {
|
|
const tracked = execFileSync(
|
|
'git',
|
|
['ls-files', '--cached', '--others', '--exclude-standard'],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
)
|
|
.trim()
|
|
.split('\n');
|
|
assert.doesNotThrow(() => classifyRuntimeImpact(tracked));
|
|
});
|
|
|
|
test('workflow CLI reads changed paths from stdin', () => {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
['scripts/runtime-impact.mjs', '--stdin'],
|
|
{ cwd: root, input: 'src/web/App.tsx\nsrc/server/dataplane.ts\n', encoding: 'utf8' },
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stdout.trim(), 'affected-components=control+dataplane\nrestart-scope=both');
|
|
});
|
|
|
|
test('push range keeps a dataplane change from an earlier commit', (t) => {
|
|
const repository = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-impact-'));
|
|
t.after(() => fs.rmSync(repository, { recursive: true, force: true }));
|
|
git(repository, 'init', '-q');
|
|
fs.writeFileSync(path.join(repository, 'README.md'), 'base\n');
|
|
git(repository, 'add', '.');
|
|
git(repository, '-c', 'user.name=Harbor', '-c', 'user.email=harbor@example.test', 'commit', '-qm', 'base');
|
|
const before = git(repository, 'rev-parse', 'HEAD');
|
|
|
|
const dataplane = path.join(repository, 'src/server/services/domainTrafficService.ts');
|
|
fs.mkdirSync(path.dirname(dataplane), { recursive: true });
|
|
fs.writeFileSync(dataplane, 'export {};\n');
|
|
git(repository, 'add', '.');
|
|
git(repository, '-c', 'user.name=Harbor', '-c', 'user.email=harbor@example.test', 'commit', '-qm', 'dataplane');
|
|
fs.writeFileSync(path.join(repository, 'README.md'), 'tip docs change\n');
|
|
git(repository, 'add', '.');
|
|
git(repository, '-c', 'user.name=Harbor', '-c', 'user.email=harbor@example.test', 'commit', '-qm', 'tip');
|
|
|
|
const changed = git(repository, 'diff', '--name-only', before, 'HEAD').split('\n');
|
|
assert.deepEqual(classifyRuntimeImpact(changed), {
|
|
affectedComponents: ['dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
});
|
|
|
|
test('rename range keeps ownership of the displaced dataplane path', (t) => {
|
|
const repository = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-impact-rename-'));
|
|
t.after(() => fs.rmSync(repository, { recursive: true, force: true }));
|
|
git(repository, 'init', '-q');
|
|
const oldPath = path.join(repository, 'src/server/services/domainTrafficService.ts');
|
|
fs.mkdirSync(path.dirname(oldPath), { recursive: true });
|
|
fs.writeFileSync(oldPath, 'export function dataplaneOwned() { return true; }\n');
|
|
git(repository, 'add', '.');
|
|
git(repository, '-c', 'user.name=Harbor', '-c', 'user.email=harbor@example.test', 'commit', '-qm', 'base');
|
|
const before = git(repository, 'rev-parse', 'HEAD');
|
|
|
|
fs.renameSync(oldPath, path.join(path.dirname(oldPath), 'newService.ts'));
|
|
git(repository, 'add', '-A');
|
|
git(repository, '-c', 'user.name=Harbor', '-c', 'user.email=harbor@example.test', 'commit', '-qm', 'rename');
|
|
const changed = git(repository, 'diff', '--no-renames', '--name-only', before, 'HEAD').split('\n');
|
|
|
|
assert.deepEqual(classifyRuntimeImpact(changed), {
|
|
affectedComponents: ['control', 'dataplane'],
|
|
restartScope: 'both',
|
|
});
|
|
});
|