You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/19 00:12:40 UTC
(maka) branch main updated: fix(runtime-host): validate project roots for the target platform (#6122)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new c8f0dae03c fix(runtime-host): validate project roots for the target platform (#6122)
c8f0dae03c is described below
commit c8f0dae03cf687ada0e0e916d090770f29a6d0f7
Author: Dan Martin <93...@users.noreply.github.com>
AuthorDate: Mon Oct 19 08:12:36 2026 +0800
fix(runtime-host): validate project roots for the target platform (#6122)
Fix valid Windows project roots such as `C:\Projects` and UNC paths being rejected by Desktop and managed-service CLI commands even though `serve` accepts them.
Desktop now validates against the target Host platform, keeping WSL/POSIX rules intact. SSH onboarding validates again after its Node platform probe. Managed-service parsing uses native Host path rules. Existing input, identity, CAS and interruption checks remain in place.
Generated-by: Codex
---
.github/workflows/windows-recovery.yml | 23 ++++++++-
.../main/__tests__/runtime-host-management.test.ts | 22 ++++++--
.../main/__tests__/runtime-host-onboarding.test.ts | 45 +++++++++++++++++
.../runtime-host-project-directory-policy.test.ts | 23 ++++++++-
.../src/main/runtime-host-local-management.ts | 1 +
.../src/main/runtime-host-management-provider.ts | 1 +
apps/desktop/src/main/runtime-host-management.ts | 12 ++---
apps/desktop/src/main/runtime-host-onboarding.ts | 12 +++--
.../settings/runtime-host-management-dialog.tsx | 2 +-
.../settings/runtime-host-onboarding-dialog.tsx | 2 +-
.../runtime-host-project-directory-policy.ts | 18 +++++--
.../__tests__/runtime-host-service-manager.test.ts | 59 ++++++++++++++++++++++
packages/cli/src/runtime-host-cli.ts | 36 ++++---------
scripts/ci-workflow-policy.test.mjs | 2 +-
scripts/windows-package-source-closure.test.mjs | 12 +++--
15 files changed, 215 insertions(+), 55 deletions(-)
diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml
index 1b490060db..8bdacde643 100644
--- a/.github/workflows/windows-recovery.yml
+++ b/.github/workflows/windows-recovery.yml
@@ -32,6 +32,8 @@ name: Windows recovery
# regression this lane has caught that no other lane could have. That second
# group is generated rather than curated, because a hand-picked list cannot be
# shown to be complete and this one has to be.
+# The CLI project-root parser is also included: node:path.isAbsolute has native
+# Windows behavior without a win32 literal in the caller.
#
# What is deliberately absent is the portable rest of those workspaces. Every
# source directory in the closure used to be here, which put this lane on 118 of
@@ -68,7 +70,17 @@ on:
# `process.platform === 'win32'`. Generated, not curated:
# `scripts/windows-package-source-closure.test.mjs` recomputes this set
# from the dist tests the steps run and fails on any difference in either
- # direction.
+ # direction, including the CLI's native path parser described above.
+ - 'packages/cli/src/__tests__/runtime-host-service-manager.test.ts'
+ - 'packages/cli/src/runtime-host-cli-installation.ts'
+ - 'packages/cli/src/runtime-host-cli.ts'
+ - 'packages/cli/src/runtime-host-lifecycle-transaction.ts'
+ - 'packages/cli/src/runtime-host-managed-deployment.ts'
+ - 'packages/cli/src/runtime-host-package-deployment.ts'
+ - 'packages/cli/src/runtime-host-peer-artifact.ts'
+ - 'packages/cli/src/runtime-host-service-management-command.ts'
+ - 'packages/cli/src/runtime-host-update-command.ts'
+ - 'packages/cli/src/runtime-host-windows-task-launcher-artifact.ts'
- 'packages/core/src/diagnostic-log.ts'
- 'packages/runtime-host/src/__tests__/control-endpoint.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/endpoint-hygiene.ts'
@@ -135,6 +147,7 @@ on:
- 'packages/storage/src/session-bundle-file-service.ts'
- 'packages/storage/src/sqlite-long-term-memory-store.ts'
- 'packages/storage/src/stable-storage.ts'
+ - 'packages/storage/src/workspace-root.ts'
- '.github/workflows/windows-recovery.yml'
# Unfiltered on purpose: required_status_checks is `strict: false`, so a pull
# request goes green against a stale base and only the merged result proves
@@ -206,6 +219,14 @@ jobs:
exit 1
}
+ # The remaining service-manager tests exercise POSIX service backends.
+ - name: Verify native Runtime Host project-root parsing
+ shell: pwsh
+ run: >-
+ node.exe --test
+ --test-name-pattern="uses native Host paths consistently|parses the bounded managed service"
+ packages/cli/dist/__tests__/runtime-host-service-manager.test.js
+
- name: Verify root initialization replacement race
shell: pwsh
run: |
diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
index a72a25fd0c..1a3b353698 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -28,6 +28,7 @@ import {
RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY,
runtimeHostAccessCredentialFingerprint,
type RuntimeHostServiceManagementFrame,
+ type RuntimeHostOperatorCommand,
} from '@maka/runtime-host/operator';
import { createDesktopRuntimeHostManagement } from '../runtime-host-management.js';
import { createDesktopRuntimeHostPeerMeshManagement } from '../runtime-host-peer-mesh-management.js';
@@ -793,7 +794,12 @@ test('publishes update progress and waits for the managed profile to reconnect',
);
});
-test('configures Project roots with CAS and reconnects only after a committed cutover', async () => {
+for (const platform of ['posix', 'win32'] as const) {
+ test(`configures ${platform} Project roots with CAS and reconnects only after a committed cutover`, () =>
+ assertConfiguresProjectRoots(platform));
+}
+
+async function assertConfiguresProjectRoots(platform: 'posix' | 'win32'): Promise<void> {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
id: 'office',
@@ -810,7 +816,9 @@ test('configures Project roots with CAS and reconnects only after a committed cu
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
- operator: OPERATOR,
+ operator: platform === 'posix' ? OPERATOR : {
+ kind: 'node' as const, platform, nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs',
+ },
};
const fingerprint = `sha256:${'c'.repeat(64)}`;
const inputs: DesktopRuntimeHostSshManagementInput[] = [];
@@ -859,7 +867,11 @@ test('configures Project roots with CAS and reconnects only after a committed cu
});
const configure = handlers.get('runtime-host-management:configure-project-directories');
assert.ok(configure);
- const roots = [{ label: 'Work', path: '/srv/work ' }];
+ const roots = [{ label: 'Work', path: platform === 'win32' ? 'C:\\work ' : '/srv/work ' }];
+ if (platform === 'posix') {
+ await assert.rejects(async () => configure({}, profile.id, [{ label: 'Work', path: 'C:\\work' }], fingerprint, false), /Project directory is invalid/u);
+ assert.equal(inputs.length, 0);
+ }
const blocked = await configure({}, profile.id, roots, fingerprint, false);
assert.equal(
(blocked as { configuration: { kind: string } }).configuration.kind,
@@ -899,7 +911,7 @@ test('configures Project roots with CAS and reconnects only after a committed cu
'The Runtime Host change was applied, but Desktop could not reconnect: authentication required',
},
);
-});
+}
test('manages one Host update policy and reconciles it through the bound operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
@@ -1431,7 +1443,7 @@ function managedSshBinding() {
function managedBinding<
Profile,
- Service extends { readonly id: string; readonly rootPath: string; readonly operator: typeof OPERATOR },
+ Service extends { readonly id: string; readonly rootPath: string; readonly operator: RuntimeHostOperatorCommand },
State extends 'active' | 'uninstalling' | 'cleanup_pending',
>(profile: Profile, service: Service, state: State) {
return {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
index 7f21b797eb..dbb74ec469 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
@@ -34,6 +34,51 @@ const OPERATOR = {
modulePath: '/home/operator/.local/share/maka/operator.mjs',
};
+test('checks SSH project directories against the probed Host platform before setup', async () => {
+ const roots = [{ label: 'Work', path: 'C:\\projects' }];
+ for (const platform of ['win32', 'linux'] as const) {
+ let setupCalls = 0;
+ const harness = createHarness({
+ resolveSshNodeIdentity: async () => ({ platform, architecture: 'x64' }),
+ profiles: { addAndEnableVerified: async (input) => ({ profileId: input.profile.id }) },
+ runSetup: async (input) => {
+ setupCalls++;
+ assert.equal(input.remotePlatform, 'win32');
+ assert.deepEqual(input.projectDirectoryRoots, roots);
+ return {
+ serviceId: 'b'.repeat(64), deploymentId: '00000000-0000-4000-8000-000000000001',
+ rootPath: 'C:\\Maka', rootId: 'a'.repeat(64),
+ operator: { kind: 'node', platform: 'win32', nodePath: 'C:\\node.exe', modulePath: 'C:\\Maka\\operator.mjs' },
+ endpoint: 'ws://127.0.0.1:7443/runtime-host', credential: 'secret-access-token',
+ };
+ },
+ });
+ try {
+ const result = await harness.invoke('runtime-host-onboarding:start', {
+ kind: 'ssh', name: 'Lab', destination: 'operator@example.com', projectDirectoryRoots: roots,
+ });
+ assert.equal((result as { kind: string }).kind, platform === 'win32' ? 'complete' : 'failed');
+ assert.equal(setupCalls, platform === 'win32' ? 1 : 0);
+ if (platform === 'linux') assert.match(JSON.stringify(result), /Project directory is invalid/u);
+ } finally {
+ await harness.onboarding.close();
+ }
+ }
+});
+
+test('rejects Windows directories for WSL before starting setup', async () => {
+ const harness = createHarness();
+ try {
+ const result = await harness.invoke('runtime-host-onboarding:start', {
+ kind: 'wsl', distribution: 'Ubuntu', projectDirectoryRoots: [{ label: 'Work', path: 'C:\\work' }],
+ });
+ assert.equal((result as { kind: string }).kind, 'failed');
+ assert.match(JSON.stringify(result), /Project directory is invalid/u);
+ } finally {
+ await harness.onboarding.close();
+ }
+});
+
test('persists a verified on-demand SSH profile without endpoint or credential projection', async () => {
let setupInput: unknown;
let saved:
diff --git a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
index 58597427bc..c1081c9c4e 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-project-directory-policy.test.ts
@@ -39,7 +39,26 @@ test('preserves opaque remote paths while normalizing labels', () => {
test('rejects paths that are not absolute on the remote POSIX Host', () => {
for (const path of ['srv/work', 'C:\\work']) {
const roots = [{ label: 'Work', path }];
- assert.equal(projectDirectoryRootsValid(roots), false);
- assert.throws(() => requireProjectDirectoryRoots(roots), /Project directory is invalid/u);
+ assert.equal(projectDirectoryRootsValid(roots, 'posix'), false);
+ assert.throws(() => requireProjectDirectoryRoots(roots, 'posix'), /Project directory is invalid/u);
+ }
+});
+
+test('validates absolute paths for the target Host rather than the Desktop platform', () => {
+ for (const path of ['C:\\work', 'C:/work', '\\\\server\\share\\work', '/work']) {
+ const roots = [{ label: 'Work', path }];
+ assert.equal(projectDirectoryRootsValid(roots, 'win32'), true, path);
+ assert.deepEqual(requireProjectDirectoryRoots(roots, 'win32'), roots);
+ assert.equal(projectDirectoryRootsValid(roots), true, path);
+ }
+ for (const platform of ['posix', 'win32', undefined] as const) {
+ for (const path of ['work', 'C:work', '', '/work\nother']) {
+ const roots = [{ label: 'Work', path }];
+ assert.equal(projectDirectoryRootsValid(roots, platform), false, path);
+ assert.throws(() => requireProjectDirectoryRoots(roots, platform), /invalid/u);
+ }
+ const duplicates = [{ label: 'Work', path: '/one' }, { label: ' Work ', path: '/two' }];
+ assert.equal(projectDirectoryRootsValid(duplicates, platform), false);
+ assert.throws(() => requireProjectDirectoryRoots(duplicates, platform), /unique/u);
}
});
diff --git a/apps/desktop/src/main/runtime-host-local-management.ts b/apps/desktop/src/main/runtime-host-local-management.ts
index c6a1f90090..904fef8766 100644
--- a/apps/desktop/src/main/runtime-host-local-management.ts
+++ b/apps/desktop/src/main/runtime-host-local-management.ts
@@ -44,6 +44,7 @@ export function createDesktopRuntimeHostLocalManagement(input: {
}): DesktopRuntimeHostManagementProvider {
return {
profileId: LOCAL_RUNTIME_HOST_PROFILE.id,
+ platform: process.platform === 'win32' ? 'win32' : 'posix',
accessManagementAvailable: false,
run: (action, allowInterruptActiveTasks) => {
const execute = (target: DesktopRuntimeHostLocalManagementTarget) =>
diff --git a/apps/desktop/src/main/runtime-host-management-provider.ts b/apps/desktop/src/main/runtime-host-management-provider.ts
index 67bf941bc5..95344efb4c 100644
--- a/apps/desktop/src/main/runtime-host-management-provider.ts
+++ b/apps/desktop/src/main/runtime-host-management-provider.ts
@@ -33,6 +33,7 @@ export type DesktopRuntimeHostManagementTerminalFrame = Exclude<
export interface DesktopRuntimeHostManagementProvider {
readonly profileId: string;
+ readonly platform: 'posix' | 'win32';
readonly accessManagementAvailable: boolean;
run(
action: Exclude<DesktopRuntimeHostManagementAction, 'uninstall'>,
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index a7fa881c88..295958bfd4 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -721,10 +721,10 @@ export function createDesktopRuntimeHostManagement(input: {
if (provider) {
const previousHostEpoch = provider.currentHostEpoch();
execute = () => provider.configureProjectDirectories(
- roots,
- expectedConfigFingerprintValue,
- allowInterruptActiveTasksValue,
- );
+ requireProjectDirectoryRoots(roots, provider.platform),
+ expectedConfigFingerprintValue,
+ allowInterruptActiveTasksValue,
+ );
reconnect = () => provider.awaitUpdatedConnection(previousHostEpoch, true);
} else {
const { managed, expectedTarget } = await activeManagedTarget(profileId);
@@ -735,7 +735,7 @@ export function createDesktopRuntimeHostManagement(input: {
operator: managed.profile.operator,
action: 'configure',
expectedTarget,
- projectDirectoryRoots: roots,
+ projectDirectoryRoots: requireProjectDirectoryRoots(roots, 'posix'),
expectedConfigFingerprint: expectedConfigFingerprintValue,
...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}),
});
@@ -747,7 +747,7 @@ export function createDesktopRuntimeHostManagement(input: {
operator: managed.control.operator,
action: 'configure',
expectedTarget,
- projectDirectoryRoots: roots,
+ projectDirectoryRoots: requireProjectDirectoryRoots(roots, managed.control.operator.kind === 'node' ? managed.control.operator.platform : 'posix'),
expectedConfigFingerprint: expectedConfigFingerprintValue,
...(allowInterruptActiveTasksValue ? { allowInterruptActiveTasks: true } : {}),
});
diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts
index 14dac44db2..80d39cf1c2 100644
--- a/apps/desktop/src/main/runtime-host-onboarding.ts
+++ b/apps/desktop/src/main/runtime-host-onboarding.ts
@@ -141,6 +141,10 @@ export function createDesktopRuntimeHostOnboarding(input: {
return await runWsl(request, setupPackage, signal);
}
const nodeIdentity = await resolveSshNodeIdentity(request, signal);
+ const remotePlatform = nodeIdentity.platform === 'win32' ? 'win32' : 'posix';
+ const projectDirectoryRoots = request.projectDirectoryRoots === undefined
+ ? undefined
+ : requireProjectDirectoryRoots(request.projectDirectoryRoots, remotePlatform);
const peerTarget = input.setupPackageMode === 'development'
? runtimeHostPeerTargetFromNode(nodeIdentity.platform, nodeIdentity.architecture)
: 'none';
@@ -166,11 +170,11 @@ export function createDesktopRuntimeHostOnboarding(input: {
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
setupPackage,
- remotePlatform: nodeIdentity.platform === 'win32' ? 'win32' : 'posix',
+ remotePlatform,
lifecycle,
principalId: `desktop:${input.clientInstanceId}`,
- ...(request.projectDirectoryRoots
- ? { projectDirectoryRoots: request.projectDirectoryRoots }
+ ...(projectDirectoryRoots
+ ? { projectDirectoryRoots }
: {}),
signal,
},
@@ -348,7 +352,7 @@ function requireOnboardingInput(value: unknown): DesktopRuntimeHostOnboardingInp
}
const roots = input.projectDirectoryRoots === undefined
? undefined
- : requireProjectDirectoryRoots(input.projectDirectoryRoots);
+ : requireProjectDirectoryRoots(input.projectDirectoryRoots, input.kind === 'wsl' ? 'posix' : undefined);
if (input.kind === 'wsl') {
if (
typeof input.distribution !== 'string' ||
diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
index 564bf0e3a4..a472d7469b 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
@@ -642,7 +642,7 @@ export function RuntimeHostManagementDialog(props: {
const automaticUpdatesAvailable = updatePolicy?.schedulingState === 'ready';
const directoryRoots = directoryPolicyEdit?.draft ?? [];
const normalizedDirectoryRoots = canonicalProjectDirectoryRoots(directoryRoots);
- const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots);
+ const directoryRootsAreValid = projectDirectoryRootsValid(directoryRoots, service?.platform === 'win32' ? 'win32' : 'posix');
const directoryRootsDirty =
directoryPolicyEdit !== undefined &&
JSON.stringify(normalizedDirectoryRoots) !== JSON.stringify(directoryPolicyEdit.baseline.roots);
diff --git a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
index f989d6dac5..ef6edfab2f 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-onboarding-dialog.tsx
@@ -89,7 +89,7 @@ export function RuntimeHostOnboardingDialog(props: {
(targetKind === 'wsl'
? distribution.trim().length > 0
: destination.trim().length > 0 && validOptionalPort(sshPort)) &&
- projectDirectoryRootsValid(projectDirectoryRoots);
+ projectDirectoryRootsValid(projectDirectoryRoots, targetKind === 'wsl' ? 'posix' : undefined);
async function start(): Promise<void> {
const roots = projectDirectoryRoots.length === 0
diff --git a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
index aedb4df26a..4289c17c22 100644
--- a/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
+++ b/apps/desktop/src/shared/runtime-host-project-directory-policy.ts
@@ -20,12 +20,22 @@
import {
canonicalProjectDirectoryRootSpec,
PROJECT_DIRECTORY_MAX_ROOTS,
- projectDirectoryPosixRootSpecValid,
+ projectDirectoryRootSpecValid,
type ProjectDirectoryRootSpec,
} from '@maka/runtime-host/protocol';
export type RuntimeHostProjectDirectoryRootInput = ProjectDirectoryRootSpec;
+// Undefined is only a preliminary check while SSH onboarding has not probed
+// the target yet. Main validates again with the discovered operator platform.
+type DirectoryPlatform = 'posix' | 'win32';
+
+function directoryRootValid(root: ProjectDirectoryRootSpec, platform?: DirectoryPlatform): boolean {
+ if (!projectDirectoryRootSpecValid(root)) return false;
+ if (root.path.startsWith('/')) return true;
+ return platform !== 'posix' && (/^[A-Za-z]:[\\/]/u.test(root.path) || root.path.startsWith('\\'));
+}
+
export function canonicalProjectDirectoryRoots(
roots: readonly RuntimeHostProjectDirectoryRootInput[],
): readonly RuntimeHostProjectDirectoryRootInput[] {
@@ -34,17 +44,19 @@ export function canonicalProjectDirectoryRoots(
export function projectDirectoryRootsValid(
roots: readonly RuntimeHostProjectDirectoryRootInput[],
+ platform?: DirectoryPlatform,
): boolean {
if (roots.length > PROJECT_DIRECTORY_MAX_ROOTS) return false;
const canonical = canonicalProjectDirectoryRoots(roots);
return (
- canonical.every(projectDirectoryPosixRootSpecValid) &&
+ canonical.every((root) => directoryRootValid(root, platform)) &&
new Set(canonical.map(({ label }) => label)).size === canonical.length
);
}
export function requireProjectDirectoryRoots(
value: unknown,
+ platform?: DirectoryPlatform,
): readonly RuntimeHostProjectDirectoryRootInput[] {
if (!Array.isArray(value) || value.length > PROJECT_DIRECTORY_MAX_ROOTS) {
throw new Error('Runtime Host Project directory policy is invalid');
@@ -62,7 +74,7 @@ export function requireProjectDirectoryRoots(
throw new Error('Runtime Host Project directory is invalid');
}
const canonical = canonicalProjectDirectoryRootSpec({ label: root.label, path: root.path });
- if (!projectDirectoryPosixRootSpecValid(canonical)) {
+ if (!directoryRootValid(canonical, platform)) {
throw new Error('Runtime Host Project directory is invalid');
}
return canonical;
diff --git a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
index 5ad9084e66..39aa10a1ef 100644
--- a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
@@ -87,6 +87,65 @@ import {
} from '../runtime-host-systemd-service.js';
describe('managed Runtime Host service', () => {
+ it('uses native Host paths consistently for serve and managed service roots', () => {
+ const paths =
+ process.platform === 'win32'
+ ? ['C:\\projects', 'C:/projects', '\\\\server\\share\\projects']
+ : ['/srv/projects'];
+ for (const path of paths) {
+ for (const [option, value] of [
+ ['--project-root', `Work=${path}`],
+ ['--project-root-json', JSON.stringify({ label: 'Work', path })],
+ ]) {
+ assert.ok(option && value);
+ assert.deepEqual(parseRuntimeHostCommand(['serve', option, value]), {
+ kind: 'runtime-host-serve',
+ json: false,
+ projectDirectoryRoots: [{ label: 'Work', path }],
+ });
+ for (const action of ['install', 'configure']) {
+ const target =
+ action === 'configure'
+ ? [
+ '--expected-service-id',
+ 'b'.repeat(64),
+ '--expected-root-path',
+ path,
+ '--expected-root-id',
+ 'a'.repeat(64),
+ '--expected-config-fingerprint',
+ `sha256:${'c'.repeat(64)}`,
+ ]
+ : [];
+ const result = parseRuntimeHostCommand(['service', action, option, value, ...target]);
+ assert.equal(result.kind, 'runtime-host-service-manage', JSON.stringify(result));
+ if (result.kind !== 'runtime-host-service-manage')
+ assert.fail('expected managed command');
+ assert.deepEqual(result.projectDirectoryRoots, [{ label: 'Work', path }]);
+ }
+ }
+ }
+ for (const path of [
+ 'relative',
+ 'C:relative',
+ ...(process.platform === 'win32' ? [] : ['C:\\projects']),
+ ]) {
+ assert.equal(
+ parseRuntimeHostCommand(['service', 'install', '--project-root', `Work=${path}`]).kind,
+ 'error',
+ );
+ assert.equal(
+ parseRuntimeHostCommand([
+ 'service',
+ 'install',
+ '--project-root-json',
+ JSON.stringify({ label: 'Work', path }),
+ ]).kind,
+ 'error',
+ );
+ }
+ });
+
it('parses the bounded managed service command surface', () => {
const nativeProjectRoot = process.platform === 'win32' ? 'C:\\projects' : '/srv/projects';
assert.deepEqual(
diff --git a/packages/cli/src/runtime-host-cli.ts b/packages/cli/src/runtime-host-cli.ts
index 3348bb9bc6..e9fb5aecfe 100644
--- a/packages/cli/src/runtime-host-cli.ts
+++ b/packages/cli/src/runtime-host-cli.ts
@@ -34,7 +34,6 @@ import {
canonicalProjectDirectoryRootSpec,
isCanonicalRuntimeHostWebSocketPath,
PROJECT_DIRECTORY_MAX_ROOTS,
- projectDirectoryPosixRootSpecValid,
projectDirectoryRootSpecValid,
} from '@maka/runtime-host/protocol';
import type { RuntimeHostManagedServiceTarget } from './runtime-host-service-manager.js';
@@ -1552,9 +1551,7 @@ function parseManagedServiceOptions(
}
projectDirectoryPolicySpecified = true;
const root =
- argument === '--project-root'
- ? parseProjectRoot(parsed, 'posix')
- : parseProjectRootJson(parsed, 'posix');
+ argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed);
if ('kind' in root) return root;
if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) {
return error(
@@ -2086,9 +2083,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand {
const parsed = optionValue(argv, index, argument);
if (typeof parsed !== 'string') return parsed;
const root =
- argument === '--project-root'
- ? parseProjectRoot(parsed, 'native')
- : parseProjectRootJson(parsed, 'native');
+ argument === '--project-root' ? parseProjectRoot(parsed) : parseProjectRootJson(parsed);
if ('kind' in root) return root;
if (projectDirectoryRoots.length >= PROJECT_DIRECTORY_MAX_ROOTS) {
return error(`--project-root may be provided at most ${PROJECT_DIRECTORY_MAX_ROOTS} times`);
@@ -2277,10 +2272,7 @@ function parseServeCommand(argv: string[]): RuntimeHostCliCommand {
};
}
-function parseProjectRoot(
- value: string,
- pathKind: 'native' | 'posix',
-): { label: string; path: string } | RuntimeHostCliError {
+function parseProjectRoot(value: string): { label: string; path: string } | RuntimeHostCliError {
const separator = value.indexOf('=');
if (separator <= 0 || separator === value.length - 1) {
return error('--project-root must use <label>=<absolute-path>');
@@ -2289,17 +2281,14 @@ function parseProjectRoot(
label: value.slice(0, separator),
path: value.slice(separator + 1),
});
- if (!projectRootValid(root, pathKind)) {
- return error(
- `--project-root must use a valid label and absolute ${pathKind === 'posix' ? 'POSIX' : 'Host'} path`,
- );
+ if (!projectRootValid(root)) {
+ return error(`--project-root must use a valid label and absolute Host path`);
}
return root;
}
function parseProjectRootJson(
value: string,
- pathKind: 'native' | 'posix',
): { label: string; path: string } | RuntimeHostCliError {
let parsed: unknown;
try {
@@ -2322,21 +2311,14 @@ function parseProjectRootJson(
label: root.label,
path: root.path,
});
- if (!projectRootValid(canonical, pathKind)) {
- return error(
- `--project-root-json must use a valid label and absolute ${pathKind === 'posix' ? 'POSIX' : 'Host'} path`,
- );
+ if (!projectRootValid(canonical)) {
+ return error(`--project-root-json must use a valid label and absolute Host path`);
}
return canonical;
}
-function projectRootValid(
- root: { readonly label: string; readonly path: string },
- pathKind: 'native' | 'posix',
-): boolean {
- return pathKind === 'posix'
- ? projectDirectoryPosixRootSpecValid(root)
- : projectDirectoryRootSpecValid(root) && isAbsolute(root.path);
+function projectRootValid(root: { readonly label: string; readonly path: string }): boolean {
+ return projectDirectoryRootSpecValid(root) && isAbsolute(root.path);
}
function parseAccessCommand(argv: string[]): RuntimeHostCliCommand {
diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs
index 3595ef3980..293ecad845 100644
--- a/scripts/ci-workflow-policy.test.mjs
+++ b/scripts/ci-workflow-policy.test.mjs
@@ -715,7 +715,7 @@ test('the recovery lane leaves the suites it executes to the required test lane'
[...workflow.matchAll(/packages[/\\]([^/\\]+)[/\\]dist[/\\]/gu)].map((match) => match[1]),
),
].sort();
- assert.deepEqual(executed, ['runtime', 'runtime-host', 'storage']);
+ assert.deepEqual(executed, ['cli', 'runtime', 'runtime-host', 'storage']);
// None of that closure belongs in the filter. These suites are ordinary
// TypeScript, so `test` runs them on Linux on every pull request and fails
diff --git a/scripts/windows-package-source-closure.test.mjs b/scripts/windows-package-source-closure.test.mjs
index 8af252ca14..f24c5e40de 100644
--- a/scripts/windows-package-source-closure.test.mjs
+++ b/scripts/windows-package-source-closure.test.mjs
@@ -93,7 +93,7 @@ test('the Windows package workflow path list has no duplicate entries', () => {
* `RELEASE_CONTRACT_FILES` so that editing the filter selects the gate that
* checks it.
*/
-test('the recovery filter is exactly the Windows-branching closure of its tests', async () => {
+test('the recovery filter is exactly the Windows-specific closure of its tests', async () => {
const workflow = readFileSync(
new URL('../.github/workflows/windows-recovery.yml', import.meta.url),
'utf8',
@@ -133,11 +133,15 @@ test('the recovery filter is exactly the Windows-branching closure of its tests'
// because it names every Windows-only path: `git-worktree-child-executor.ts`
// carries a genuine Windows workaround with no such literal. A branch written
// without it stays outside this filter, and the lane's schedule is what
- // observes it.
+ // observes it. The CLI project-root parser is an explicit exception: its
+ // node:path.isAbsolute call dispatches to Windows semantics without a win32
+ // literal, and the native Host path regression above exercises that behavior.
const closure = await collectWorkspaceSourceClosure(entrypoints);
const windowsBranching = closure
- .filter((path) =>
- readFileSync(new URL(`../${path}`, import.meta.url), 'utf8').includes('win32'),
+ .filter(
+ (path) =>
+ path === 'packages/cli/src/runtime-host-cli.ts' ||
+ readFileSync(new URL(`../${path}`, import.meta.url), 'utf8').includes('win32'),
)
.sort();
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org