Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,211 @@ test('optional dependency ownership rejects undeclared direct feature owners', a
);
});

test('services-core capability profiles keep heavy owners out of the empty profile', async () => {
const { coreClosedFeatureProfileRules } = await import(
'./core-boundaries/rules/feature-rules.mjs'
);
const { dependencyProfileRules } = await import(
'./core-boundaries/rules/crate-rules.mjs'
);
const { requiredContentRules } = await import(
'./core-boundaries/rules/source/required-rules.mjs'
);
const serviceManifest = 'src/crates/services/services-core/Cargo.toml';
const profiles = new Map(
coreClosedFeatureProfileRules
.filter((rule) => rule.manifestPath === serviceManifest)
.map((rule) => [rule.featureName, rule.requiredFeatureRefs]),
);

assert.deepEqual(profiles.get('filesystem'), [
'dep:base64',
'dep:chrono',
'dep:ignore',
'dep:sha2',
'tokio/fs',
]);
assert.deepEqual(profiles.get('local-storage'), [
'dep:bitfun-core-types',
'dep:bitfun-events',
'dep:chrono',
'dep:fs2',
'dep:libc',
'dep:sha2',
'dep:windows',
'tokio/fs',
'tokio/sync',
'windows/Win32_Foundation',
'windows/Win32_Storage_FileSystem',
]);
assert.deepEqual(profiles.get('process-runtime'), [
'dep:libc',
'dep:which',
'dep:win32job',
'dep:windows',
'tokio/io-util',
'tokio/process',
'windows/Win32_Foundation',
'windows/Win32_System_Diagnostics_ToolHelp',
'windows/Win32_System_Threading',
]);
assert.deepEqual(profiles.get('workspace-instructions'), [
'dep:globset',
'tokio/fs',
'tokio/io-util',
]);
assert.deepEqual(profiles.get('lsp'), [
'dep:anyhow',
'dep:bitfun-core-types',
'dep:notify',
'dep:zip',
'process-runtime',
'tokio/fs',
'tokio/io-util',
'tokio/sync',
]);
assert.deepEqual(profiles.get('workspace-runtime'), [
'dep:anyhow',
'dep:async-trait',
'dep:bitfun-runtime-ports',
'dep:dunce',
'process-runtime',
'tokio/fs',
'tokio/io-util',
'tokio/sync',
]);

const defaultProfile = dependencyProfileRules.find(
(rule) => rule.crateName === 'services-core',
);
for (const dependency of [
'base64',
'bitfun-core-types',
'bitfun-events',
'chrono',
'fs2',
'globset',
'ignore',
'libc',
'sha2',
'which',
'win32job',
'windows',
]) {
assert.ok(
defaultProfile?.forbiddenNonOptionalDeps.includes(dependency),
`services-core empty profile must reject ambient ${dependency}`,
);
}

const sourceRule = requiredContentRules.find(
(rule) => rule.path === 'src/crates/services/services-core/src/lib.rs',
);
const sourceContracts = sourceRule?.patterns.map((pattern) => pattern.regex.source).join('\n') ?? '';
for (const moduleName of [
'filesystem',
'json_store',
'managed_runtime',
'persistence',
'process_manager',
'process_tree',
'session',
'session_usage',
'storage_cleanup',
'system',
'token_usage',
'workspace_instructions',
]) {
assert.match(
sourceContracts,
new RegExp(`pub mod ${moduleName}`),
`services-core source rule must protect the ${moduleName} capability gate`,
);
}
});

test('services-core Tokio capabilities stay owner-scoped', () => {
const invalidPackage = {
name: 'bitfun-services-core',
manifest_path: 'src/crates/services/services-core/Cargo.toml',
dependencies: [
{
name: 'tokio',
kind: null,
optional: false,
features: ['fs', 'io-util', 'process', 'rt', 'sync', 'time'],
},
],
features: {
filesystem: [],
'local-storage': [],
'process-runtime': [],
'workspace-instructions': [],
lsp: [],
'workspace-runtime': [],
},
};

const messages = findTokioDependencyFeatureViolations([invalidPackage]).map(
(violation) => violation.message,
);
assert.ok(
messages.some((message) => message.includes('unexpected base Tokio capabilities')),
'services-core must reject ambient fs/io/process/sync Tokio capabilities',
);
assert.ok(
messages.some((message) => message.includes('filesystem missing effective Tokio capabilities: fs')),
'services-core must require filesystem to own tokio/fs',
);
assert.ok(
messages.some((message) => message.includes('lsp missing effective Tokio capabilities')),
'services-core must require lsp to declare its complete effective Tokio profile',
);
});

test('services-core Windows API capabilities stay feature-owned', async () => {
const { findServicesCorePlatformDependencyFeatureViolations } = await import(
'./core-boundaries/cargo-dependency-boundaries.mjs'
);
assert.equal(
typeof findServicesCorePlatformDependencyFeatureViolations,
'function',
'Cargo boundary checker must expose the services-core platform dependency policy',
);
const packageWithAmbientWindowsApis = {
name: 'bitfun-services-core',
manifest_path: 'src/crates/services/services-core/Cargo.toml',
dependencies: [
{
name: 'windows',
kind: null,
optional: true,
target: 'cfg(windows)',
features: ['Win32_Storage_FileSystem', 'Win32_System_Threading'],
},
],
};

const violations = findServicesCorePlatformDependencyFeatureViolations([
packageWithAmbientWindowsApis,
]);
assert.equal(violations.length, 1);
assert.match(
violations[0].message,
/windows API capabilities must be selected by services-core owner features/,
);

assert.deepEqual(
findServicesCorePlatformDependencyFeatureViolations([
{
...packageWithAmbientWindowsApis,
dependencies: [{ ...packageWithAmbientWindowsApis.dependencies[0], features: [] }],
},
]),
[],
);
});

test('closed feature profiles reject product-full hidden behind a child feature', async () => {
const { unexpectedReachableLocalFeatures } = await import(
'./core-boundaries/manifest-feature-helpers.mjs'
Expand Down
75 changes: 71 additions & 4 deletions scripts/core-boundaries/cargo-dependency-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ const SERVICES_INTEGRATIONS_TOKIO_FEATURES = new Map([
['script-tool-runtime', ['io-util', 'process', 'rt', 'sync', 'time']],
]);

const SERVICES_CORE_TOKIO_FEATURES = new Map([
['filesystem', ['fs']],
['local-storage', ['fs', 'sync']],
['process-runtime', ['io-util', 'process']],
['workspace-instructions', ['fs', 'io-util']],
['lsp', ['fs', 'io-util', 'process', 'sync']],
['workspace-runtime', ['fs', 'io-util', 'process', 'sync']],
]);
const SERVICES_CORE_BASE_TOKIO_FEATURES = ['rt', 'time'];

// The installer is an excluded standalone workspace with its own Rust checks
// and packaging lifecycle; this policy governs the root product workspace.
const TOKIO_DEPENDENCY_POLICY_EXCLUDED_PACKAGES = new Set(['bitfun-installer']);
Expand All @@ -105,11 +115,11 @@ function effectiveTokioCapabilities(feature, featureGraph, visiting = new Set())
return capabilities;
}

export function findServicesIntegrationsTokioFeatureViolations(pkg) {
function findOwnedTokioFeatureViolations(pkg, ownerProfiles) {
const violations = [];
const featureGraph = pkg.features ?? {};

for (const [feature, expectedCapabilities] of SERVICES_INTEGRATIONS_TOKIO_FEATURES) {
for (const [feature, expectedCapabilities] of ownerProfiles) {
if (!Object.hasOwn(featureGraph, feature)) {
violations.push({
path: pkg.manifest_path,
Expand Down Expand Up @@ -140,7 +150,7 @@ export function findServicesIntegrationsTokioFeatureViolations(pkg) {
}

for (const [feature, values] of Object.entries(featureGraph)) {
if (SERVICES_INTEGRATIONS_TOKIO_FEATURES.has(feature)) {
if (ownerProfiles.has(feature)) {
continue;
}
if (values.some((value) => value.startsWith('tokio/'))) {
Expand All @@ -155,6 +165,37 @@ export function findServicesIntegrationsTokioFeatureViolations(pkg) {
return violations;
}

export function findServicesIntegrationsTokioFeatureViolations(pkg) {
return findOwnedTokioFeatureViolations(pkg, SERVICES_INTEGRATIONS_TOKIO_FEATURES);
}

export function findServicesCoreTokioFeatureViolations(pkg) {
return findOwnedTokioFeatureViolations(pkg, SERVICES_CORE_TOKIO_FEATURES);
}

export function findServicesCorePlatformDependencyFeatureViolations(packages) {
const violations = [];

for (const pkg of packages) {
if (pkg.name !== 'bitfun-services-core') {
continue;
}
for (const dependency of pkg.dependencies ?? []) {
if (dependency.name !== 'windows' || (dependency.features ?? []).length === 0) {
continue;
}
violations.push({
path: pkg.manifest_path,
line: 1,
message:
'windows API capabilities must be selected by services-core owner features, not the dependency declaration',
});
}
}

return violations;
}

export function findTokioDependencyFeatureViolations(packages) {
const violations = [];

Expand All @@ -177,7 +218,29 @@ export function findTokioDependencyFeatureViolations(packages) {
const featureOwnedIntegrationRuntime =
pkg.name === 'bitfun-services-integrations'
&& (dependency.kind ?? null) === null;
if (features.length === 0 && !featureOwnedIntegrationRuntime) {
const featureOwnedServicesCoreRuntime =
pkg.name === 'bitfun-services-core'
&& (dependency.kind ?? null) === null;
if (featureOwnedServicesCoreRuntime) {
const actual = [...features].sort();
const expected = [...SERVICES_CORE_BASE_TOKIO_FEATURES].sort();
const missing = expected.filter((feature) => !actual.includes(feature));
const unexpected = actual.filter((feature) => !expected.includes(feature));
if (missing.length > 0) {
violations.push({
path: pkg.manifest_path,
line: 1,
message: `${pkg.name} missing base Tokio capabilities: ${missing.join(', ')}`,
});
}
if (unexpected.length > 0) {
violations.push({
path: pkg.manifest_path,
line: 1,
message: `${pkg.name} has unexpected base Tokio capabilities: ${unexpected.join(', ')}`,
});
}
} else if (features.length === 0 && !featureOwnedIntegrationRuntime) {
violations.push({
path: pkg.manifest_path,
line: 1,
Expand All @@ -189,6 +252,9 @@ export function findTokioDependencyFeatureViolations(packages) {
if (pkg.name === 'bitfun-services-integrations') {
violations.push(...findServicesIntegrationsTokioFeatureViolations(pkg));
}
if (pkg.name === 'bitfun-services-core') {
violations.push(...findServicesCoreTokioFeatureViolations(pkg));
}
}

return violations;
Expand Down Expand Up @@ -728,6 +794,7 @@ export function checkCargoDependencyBoundaries({ root, crateLayoutRules }) {
),
...findFeatureGatedTestTargetViolations(packages),
...findTokioDependencyFeatureViolations(packages),
...findServicesCorePlatformDependencyFeatureViolations(packages),
];
}

Expand Down
12 changes: 12 additions & 0 deletions scripts/core-boundaries/rules/crate-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,24 @@ export const dependencyProfileRules = [
forbiddenNonOptionalDeps: [
'anyhow',
'async-trait',
'base64',
'bitfun-core-types',
'bitfun-events',
'bitfun-runtime-ports',
'chrono',
'dunce',
'fs2',
'git2',
'globset',
'ignore',
'libc',
'notify',
'rusqlite',
'serde_yaml',
'sha2',
'which',
'win32job',
'windows',
'zip',
],
},
Expand Down
Loading
Loading