mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(secrets): preserve Windows ACL diagnostics (#120211)
* fix(secrets): preserve Windows ACL diagnostics * fix(secrets): make Windows path security proof deterministic * test(secrets): isolate Windows ACL tool failures * test(secrets): preserve ACL preload process contract * test(ci): route Doctor ACL proof to Windows * test(qa): normalize Clack note borders * test(ci): register Windows ACL preload for deadcode checks
This commit is contained in:
@@ -56,6 +56,8 @@ const ROOT_TEST_ENTRY_GLOBS = [
|
||||
// Invoked directly by the Docker image-auth scenario.
|
||||
"test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts!",
|
||||
"test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts!",
|
||||
// Loaded through NODE_OPTIONS by the Doctor SecretRef E2E; no static import edge exists.
|
||||
"test/fixtures/windows-acl-tools-unavailable.mjs!",
|
||||
// QA scenario YAML dispatches these scripts/tests by path rather than import.
|
||||
...QA_SCENARIO_EXECUTION_ENTRIES,
|
||||
// Invoked directly by the sandbox bind-conflict E2E verification script.
|
||||
|
||||
+1
-1
@@ -1905,7 +1905,7 @@
|
||||
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
|
||||
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
|
||||
"test:watch": "node scripts/test-projects.mjs --watch",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:schtasks:integration": "node scripts/run-with-env.mjs CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
|
||||
|
||||
@@ -53,6 +53,10 @@ const WINDOWS_SCOPE_RE =
|
||||
/^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:exec-allowlist-pattern|fs-safe-remove)(?:\.test)?|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mjs|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|js)|lib\/(?:direct-run\.mjs|format-generated-module\.mjs|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
|
||||
const WINDOWS_TEST_SCOPE_RE =
|
||||
/^(extensions\/mxc\/test\/(?:mxc-backend|sandbox-policy-loader)\.test\.ts$|src\/agents\/bash-tools\.exec\.script-preflight\.test\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive\.worker|store\.session-lifecycle-mutation)\.test\.ts$|src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/(?:exec-allowlist-pattern|fs-safe-remove|ssh-client\.windows|update-managed-service-handoff-(?:command|lifecycle)|windows-install-roots)\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|src\/state\/openclaw-database-paths\.windows\.test\.ts$|src\/test-utils\/openclaw-test-state\.test\.ts$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
|
||||
const WINDOWS_SECRETREF_SCOPE_RE =
|
||||
/^(?:src\/commands\/doctor-gateway-auth-token(?:\.windows\.test)?\.ts|src\/flows\/(?:doctor-core-checks|doctor-health-contributions)\.ts|src\/gateway\/(?:auth-token-resolution|resolve-configured-secret-input-string)\.ts|src\/infra\/(?:fs-safe|fs-safe-defaults|permissions)\.ts|src\/secrets\/(?:resolve|resolve-errors|test-node-command\.test-support)\.ts|src\/security\/audit-fs\.ts|src\/test-utils\/vitest-spies\.ts|test\/e2e\/qa-lab\/runtime\/doctor-auth-secretref-checks\.e2e\.test\.ts|test\/fixtures\/windows-acl-tools-unavailable\.mjs)$/;
|
||||
const WINDOWS_SECRETREF_TEST_SCOPE_RE =
|
||||
/^(?:src\/commands\/doctor-gateway-auth-token\.windows\.test\.ts|src\/secrets\/test-node-command\.test-support\.ts|test\/e2e\/qa-lab\/runtime\/doctor-auth-secretref-checks\.e2e\.test\.ts|test\/fixtures\/windows-acl-tools-unavailable\.mjs)$/;
|
||||
const WINDOWS_DAEMON_SCOPE_RE =
|
||||
/^src\/daemon\/(?:schtasks(?:[-.][^/]+)?|runtime-hints\.windows-paths(?:\.test)?|test-helpers\/schtasks-(?:base-mocks|fixtures))\.ts$/;
|
||||
const CONTROL_UI_I18N_SCOPE_RE =
|
||||
@@ -158,8 +162,12 @@ export function detectChangedScope(changedPaths) {
|
||||
if (
|
||||
(WINDOWS_SCOPE_RE.test(path) ||
|
||||
WINDOWS_SQLITE_SCOPE_RE.test(path) ||
|
||||
WINDOWS_SECRETREF_SCOPE_RE.test(path) ||
|
||||
WINDOWS_DAEMON_SCOPE_RE.test(path)) &&
|
||||
(!facts.isTestOnly || WINDOWS_TEST_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path))
|
||||
(!facts.isTestOnly ||
|
||||
WINDOWS_TEST_SCOPE_RE.test(path) ||
|
||||
WINDOWS_SECRETREF_TEST_SCOPE_RE.test(path) ||
|
||||
WINDOWS_DAEMON_SCOPE_RE.test(path))
|
||||
) {
|
||||
runWindows = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Windows Doctor service-token tests cover detailed SecretRef diagnostic rendering.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveGatewayAuthToken: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/auth-token-resolution.js", () => ({
|
||||
resolveGatewayAuthToken: mocks.resolveGatewayAuthToken,
|
||||
}));
|
||||
|
||||
const { resolveGatewayAuthTokenForService } = await import("./doctor-gateway-auth-token.js");
|
||||
|
||||
describe("resolveGatewayAuthTokenForService Windows diagnostics", () => {
|
||||
beforeEach(() => {
|
||||
mocks.resolveGatewayAuthToken.mockReset();
|
||||
});
|
||||
|
||||
it("preserves the detailed path-free ACL recovery from the canonical resolver", async () => {
|
||||
const privateCommand = String.raw`C:\private\gateway-token-provider.cmd`;
|
||||
const recovery =
|
||||
"Windows path security could not be verified. Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify.";
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
auth: {
|
||||
token: {
|
||||
source: "exec",
|
||||
provider: "execmain",
|
||||
id: "gateway/token",
|
||||
},
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: privateCommand,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const env = {} as NodeJS.ProcessEnv;
|
||||
mocks.resolveGatewayAuthToken.mockResolvedValue({
|
||||
secretRefConfigured: true,
|
||||
unresolvedRefReason: recovery,
|
||||
});
|
||||
|
||||
const resolved = await resolveGatewayAuthTokenForService(cfg, env, {
|
||||
allowExecSecretRefs: true,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
unavailableReason: `gateway.auth.token SecretRef is configured but unresolved (${recovery}).`,
|
||||
});
|
||||
expect(resolved.unavailableReason).not.toContain(privateCommand);
|
||||
expect(mocks.resolveGatewayAuthToken).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
env,
|
||||
unresolvedReasonStyle: "detailed",
|
||||
envFallback: "always",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,27 @@
|
||||
/**
|
||||
* Tests configured secret input resolution for gateway method parameters.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { withMockedWindowsAclVerificationUnavailable } from "../test-utils/vitest-spies.js";
|
||||
import {
|
||||
resolveConfiguredSecretInputWithFallback,
|
||||
resolveRequiredConfiguredSecretRefInputString,
|
||||
} from "./resolve-configured-secret-input-string.js";
|
||||
|
||||
let fixtureRoot = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-secret-input-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function createConfig(value: unknown): OpenClawConfig {
|
||||
return {
|
||||
gateway: {
|
||||
@@ -23,6 +37,26 @@ function createConfig(value: unknown): OpenClawConfig {
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
async function createWindowsAclUnavailableConfig() {
|
||||
const filePath = path.join(fixtureRoot, "gateway-token.txt");
|
||||
await fs.writeFile(filePath, "secret-token", { mode: 0o600 });
|
||||
return {
|
||||
filePath,
|
||||
config: {
|
||||
gateway: {
|
||||
auth: {
|
||||
token: { source: "file", provider: "filemain", id: "value" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: { source: "file", path: filePath, mode: "singleValue" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveConfiguredSecretInputWithFallback", () => {
|
||||
it("returns plaintext config value when present", async () => {
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
@@ -147,6 +181,60 @@ describe("resolveConfiguredSecretInputWithFallback", () => {
|
||||
expect(resolved.unresolvedRefReason).toContain("gateway.auth.token SecretRef is unresolved");
|
||||
expect(resolved.unresolvedRefReason).toContain("MISSING_GATEWAY_TOKEN");
|
||||
});
|
||||
|
||||
it("keeps generic Windows ACL failures byte-compatible", async () => {
|
||||
await withMockedWindowsAclVerificationUnavailable(
|
||||
path.join(fixtureRoot, "missing-windows-system-root"),
|
||||
async () => {
|
||||
const { config } = await createWindowsAclUnavailableConfig();
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
value: config.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
});
|
||||
|
||||
expect(resolved.unresolvedRefReason).toBe(
|
||||
"gateway.auth.token SecretRef is unresolved (file:filemain:value).",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("adds only the sanitized Windows ACL diagnostic in detailed mode", async () => {
|
||||
await withMockedWindowsAclVerificationUnavailable(
|
||||
path.join(fixtureRoot, "missing-windows-system-root"),
|
||||
async () => {
|
||||
const { config, filePath } = await createWindowsAclUnavailableConfig();
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
value: config.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
|
||||
expect(resolved.unresolvedRefReason).toBe(
|
||||
"gateway.auth.token SecretRef is unresolved (file:filemain:value). Windows path security could not be verified. Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify.",
|
||||
);
|
||||
expect(resolved.unresolvedRefReason).not.toContain(filePath);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps unrelated detailed provider failures unchanged", async () => {
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
config: createConfig("${MISSING_GATEWAY_TOKEN}"),
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
value: "${MISSING_GATEWAY_TOKEN}",
|
||||
path: "gateway.auth.token",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
|
||||
expect(resolved.unresolvedRefReason).toBe(
|
||||
"gateway.auth.token SecretRef is unresolved (env:default:MISSING_GATEWAY_TOKEN).",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRequiredConfiguredSecretRefInputString", () => {
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { secretRefKey } from "../secrets/ref-contract.js";
|
||||
import {
|
||||
describeSecretResolutionOperatorDiagnostic,
|
||||
describeSecretResolutionOperatorRecovery,
|
||||
} from "../secrets/resolve-errors.js";
|
||||
import { resolveSecretRefValues } from "../secrets/resolve.js";
|
||||
|
||||
export type SecretInputUnresolvedReasonStyle = "generic" | "detailed"; // pragma: allowlist secret
|
||||
@@ -78,14 +82,22 @@ export async function resolveConfiguredSecretInputString(params: {
|
||||
};
|
||||
}
|
||||
return { value: trimmed };
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const operatorDiagnostic =
|
||||
style === "detailed" ? describeSecretResolutionOperatorDiagnostic(error) : undefined;
|
||||
const operatorRecovery =
|
||||
style === "detailed" ? describeSecretResolutionOperatorRecovery(error) : undefined;
|
||||
const unresolvedReason = buildUnresolvedReason({
|
||||
path: params.path,
|
||||
style,
|
||||
kind: "unresolved",
|
||||
refLabel,
|
||||
});
|
||||
const operatorDetail = [operatorDiagnostic, operatorRecovery].filter(Boolean).join(". ");
|
||||
return {
|
||||
unresolvedRefReason: buildUnresolvedReason({
|
||||
path: params.path,
|
||||
style,
|
||||
kind: "unresolved",
|
||||
refLabel,
|
||||
}),
|
||||
unresolvedRefReason: operatorDetail
|
||||
? `${unresolvedReason} ${operatorDetail}.`
|
||||
: unresolvedReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,4 +114,39 @@ describe("detectChangedScope Windows routing", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("routes SecretRef path-security changes and native fixtures to Windows", () => {
|
||||
for (const secretRefPath of [
|
||||
"src/commands/doctor-gateway-auth-token.ts",
|
||||
"src/commands/doctor-gateway-auth-token.windows.test.ts",
|
||||
"src/flows/doctor-core-checks.ts",
|
||||
"src/flows/doctor-health-contributions.ts",
|
||||
"src/gateway/auth-token-resolution.ts",
|
||||
"src/gateway/resolve-configured-secret-input-string.ts",
|
||||
"src/infra/fs-safe.ts",
|
||||
"src/infra/fs-safe-defaults.ts",
|
||||
"src/infra/permissions.ts",
|
||||
"src/secrets/resolve-errors.ts",
|
||||
"src/secrets/resolve.ts",
|
||||
"src/secrets/test-node-command.test-support.ts",
|
||||
"src/security/audit-fs.ts",
|
||||
"src/test-utils/vitest-spies.ts",
|
||||
"test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts",
|
||||
"test/fixtures/windows-acl-tools-unavailable.mjs",
|
||||
]) {
|
||||
expect(detectChangedScope([secretRefPath]), secretRefPath).toMatchObject({
|
||||
runNode: true,
|
||||
runWindows: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("does not route SecretRef unit tests omitted from the Windows shard", () => {
|
||||
for (const testPath of [
|
||||
"src/gateway/resolve-configured-secret-input-string.test.ts",
|
||||
"src/secrets/resolve.test.ts",
|
||||
]) {
|
||||
expect(detectChangedScope([testPath]).runWindows, testPath).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ type SecretRefResolutionCode =
|
||||
| "SECRET_REF_PROVIDER_CONTRACT";
|
||||
|
||||
type SecretProviderResolutionCode =
|
||||
| "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE"
|
||||
| "SECRET_PROVIDER_INVALID"
|
||||
| "SECRET_PROVIDER_NOT_CONFIGURED"
|
||||
| "SECRET_PROVIDER_UNAVAILABLE";
|
||||
@@ -86,7 +87,10 @@ export function describeSecretResolutionError(
|
||||
value: unknown,
|
||||
): SecretResolutionFailureReason | undefined {
|
||||
if (value instanceof SecretProviderResolutionError) {
|
||||
return value.code === "SECRET_PROVIDER_UNAVAILABLE" ? "secret provider failed" : undefined;
|
||||
return value.code === "SECRET_PROVIDER_UNAVAILABLE" ||
|
||||
value.code === "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE"
|
||||
? "secret provider failed"
|
||||
: undefined;
|
||||
}
|
||||
if (!(value instanceof SecretRefResolutionError)) {
|
||||
return undefined;
|
||||
@@ -106,6 +110,30 @@ export function describeSecretResolutionError(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Sanitized provider detail suitable for operator-facing diagnostics. */
|
||||
export function describeSecretResolutionOperatorDiagnostic(value: unknown): string | undefined {
|
||||
if (
|
||||
value instanceof SecretProviderResolutionError &&
|
||||
value.code === "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE"
|
||||
) {
|
||||
return "Windows path security could not be verified";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Sanitized recovery action suitable for operator-facing diagnostics. */
|
||||
export function describeSecretResolutionOperatorRecovery(value: unknown): string | undefined {
|
||||
if (
|
||||
!(value instanceof SecretProviderResolutionError) ||
|
||||
value.code !== "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return value.source === "exec"
|
||||
? "Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify"
|
||||
: "Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify";
|
||||
}
|
||||
|
||||
export function providerResolutionError(params: {
|
||||
code?: SecretProviderResolutionCode;
|
||||
source: SecretRefSource;
|
||||
|
||||
+140
-11
@@ -12,9 +12,18 @@ import {
|
||||
writeForkingNoOutputScript,
|
||||
} from "../test-utils/process-tree.js";
|
||||
import { INVALID_EXEC_SECRET_REF_IDS } from "../test-utils/secret-ref-test-vectors.js";
|
||||
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
|
||||
import {
|
||||
withMockedWindowsAclVerificationUnavailable,
|
||||
withMockedWindowsPlatform,
|
||||
} from "../test-utils/vitest-spies.js";
|
||||
import {
|
||||
describeSecretResolutionError,
|
||||
describeSecretResolutionOperatorDiagnostic,
|
||||
describeSecretResolutionOperatorRecovery,
|
||||
} from "./resolve-errors.js";
|
||||
import {
|
||||
isMissingSecretRefResolutionError,
|
||||
isProviderScopedSecretResolutionError,
|
||||
resolveSecretRefString,
|
||||
resolveSecretRefValue,
|
||||
resolveSecretRefValues,
|
||||
@@ -793,13 +802,14 @@ describe("secret ref resolver", () => {
|
||||
});
|
||||
|
||||
it("fails closed on Windows when file provider ACL source is unknown", async () => {
|
||||
await withMockedWindowsPlatform(async () => {
|
||||
const dir = await createCaseDir("win-acl");
|
||||
const filePath = path.join(dir, "secrets.json");
|
||||
await writeSecureFile(filePath, '{"token":"abc123"}');
|
||||
const dir = await createCaseDir("win-acl");
|
||||
await withMockedWindowsAclVerificationUnavailable(
|
||||
path.join(dir, "missing-windows-system-root"),
|
||||
async () => {
|
||||
const filePath = path.join(dir, "secrets.json");
|
||||
await writeSecureFile(filePath, '{"token":"abc123"}');
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
const error = await resolveSecretRefString(
|
||||
{ source: "file", provider: "filemain", id: "/token" },
|
||||
{
|
||||
config: {
|
||||
@@ -810,16 +820,135 @@ describe("secret ref resolver", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(/ACL verification unavailable on Windows/);
|
||||
).catch((caught: unknown) => caught);
|
||||
|
||||
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
|
||||
if (!isProviderScopedSecretResolutionError(error)) {
|
||||
return;
|
||||
}
|
||||
expect(error.code).toBe("SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE");
|
||||
expect(describeSecretResolutionError(error)).toBe("secret provider failed");
|
||||
expect(describeSecretResolutionOperatorDiagnostic(error)).toBe(
|
||||
"Windows path security could not be verified",
|
||||
);
|
||||
expect(describeSecretResolutionOperatorRecovery(error)).toBe(
|
||||
"Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a missing Windows file provider path as a generic failure", async () => {
|
||||
await withMockedWindowsPlatform(async () => {
|
||||
const dir = await createCaseDir("win-file-missing");
|
||||
const filePath = path.join(dir, "missing.json");
|
||||
const error = await resolveSecretRefString(
|
||||
{ source: "file", provider: "filemain", id: "/token" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: createFileProviderConfig(filePath),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
).catch((caught: unknown) => caught);
|
||||
|
||||
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
|
||||
if (!isProviderScopedSecretResolutionError(error)) {
|
||||
return;
|
||||
}
|
||||
expect(error.code).toBe("SECRET_PROVIDER_UNAVAILABLE");
|
||||
expect(describeSecretResolutionError(error)).toBe("secret provider failed");
|
||||
expect(describeSecretResolutionOperatorDiagnostic(error)).toBeUndefined();
|
||||
expect(describeSecretResolutionOperatorRecovery(error)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed on Windows when exec provider ACL source is unknown", async () => {
|
||||
const dir = await createCaseDir("win-exec-acl");
|
||||
await withMockedWindowsAclVerificationUnavailable(
|
||||
path.join(dir, "missing-windows-system-root"),
|
||||
async () => {
|
||||
const markerPath = path.join(dir, "executed");
|
||||
const commandPath = path.join(dir, "resolver.sh");
|
||||
await writeSecureFile(
|
||||
commandPath,
|
||||
["#!/bin/sh", `touch ${JSON.stringify(markerPath)}`].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
const error = await resolveExecSecret(commandPath).catch((caught: unknown) => caught);
|
||||
|
||||
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
|
||||
if (!isProviderScopedSecretResolutionError(error)) {
|
||||
return;
|
||||
}
|
||||
expect(error.code).toBe("SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE");
|
||||
expect(describeSecretResolutionError(error)).toBe("secret provider failed");
|
||||
expect(describeSecretResolutionOperatorDiagnostic(error)).toBe(
|
||||
"Windows path security could not be verified",
|
||||
);
|
||||
expect(describeSecretResolutionOperatorRecovery(error)).toBe(
|
||||
"Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify",
|
||||
);
|
||||
await expect(fs.access(markerPath)).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a missing Windows exec provider path as a generic failure", async () => {
|
||||
await withMockedWindowsPlatform(async () => {
|
||||
await expect(resolveExecSecret(execProtocolV1ScriptPath)).rejects.toThrow(
|
||||
/Move the command to a path whose ACLs OpenClaw can verify; there is no provider-level bypass/,
|
||||
const dir = await createCaseDir("win-exec-missing");
|
||||
const commandPath = path.join(dir, "missing-resolver");
|
||||
const error = await resolveExecSecret(commandPath).catch((caught: unknown) => caught);
|
||||
|
||||
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
|
||||
if (!isProviderScopedSecretResolutionError(error)) {
|
||||
return;
|
||||
}
|
||||
expect(error.code).toBe("SECRET_PROVIDER_UNAVAILABLE");
|
||||
expect(describeSecretResolutionError(error)).toBe("secret provider failed");
|
||||
expect(describeSecretResolutionOperatorDiagnostic(error)).toBeUndefined();
|
||||
expect(describeSecretResolutionOperatorRecovery(error)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a failed Windows exec permission restat as a generic failure", async () => {
|
||||
await withMockedWindowsPlatform(async () => {
|
||||
const dir = await createCaseDir("win-exec-restat");
|
||||
const markerPath = path.join(dir, "executed");
|
||||
const commandPath = path.join(dir, "resolver.sh");
|
||||
await writeSecureFile(
|
||||
commandPath,
|
||||
["#!/bin/sh", `touch ${JSON.stringify(markerPath)}`].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
const originalLstat = fs.lstat.bind(fs);
|
||||
let commandPathStats = 0;
|
||||
const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (...args) => {
|
||||
if (String(args[0]) === commandPath && ++commandPathStats === 2) {
|
||||
throw Object.assign(new Error("provider command disappeared"), { code: "ENOENT" });
|
||||
}
|
||||
return await originalLstat(...args);
|
||||
});
|
||||
try {
|
||||
const error = await resolveExecSecret(commandPath).catch((caught: unknown) => caught);
|
||||
|
||||
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
|
||||
if (!isProviderScopedSecretResolutionError(error)) {
|
||||
return;
|
||||
}
|
||||
expect(error.code).toBe("SECRET_PROVIDER_UNAVAILABLE");
|
||||
expect(describeSecretResolutionError(error)).toBe("secret provider failed");
|
||||
expect(describeSecretResolutionOperatorDiagnostic(error)).toBeUndefined();
|
||||
expect(describeSecretResolutionOperatorRecovery(error)).toBeUndefined();
|
||||
await expect(fs.access(markerPath)).rejects.toThrow();
|
||||
} finally {
|
||||
lstatSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-1
@@ -99,7 +99,17 @@ function throwUnknownProviderResolutionError(params: {
|
||||
if (isSecretResolutionError(params.err)) {
|
||||
throw params.err;
|
||||
}
|
||||
// fs-safe 0.5 exposes one fail-closed receipt for failed permission inspection,
|
||||
// unavailable ACL data, and indeterminate owner trust. Keep the diagnostic path-free.
|
||||
const isWindowsPathSecurityFailure =
|
||||
process.platform === "win32" &&
|
||||
(params.source === "file" || params.source === "exec") &&
|
||||
params.err instanceof FsSafeError &&
|
||||
params.err.code === "permission-unverified";
|
||||
throw providerResolutionError({
|
||||
...(isWindowsPathSecurityFailure
|
||||
? { code: "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE" as const }
|
||||
: {}),
|
||||
source: params.source,
|
||||
provider: params.provider,
|
||||
message: formatErrorMessage(params.err),
|
||||
@@ -245,7 +255,8 @@ async function assertSecurePath(params: {
|
||||
}
|
||||
|
||||
if (process.platform === "win32" && perms.source === "unknown") {
|
||||
throw new Error(
|
||||
throw new FsSafeError(
|
||||
"permission-unverified",
|
||||
`${params.label} ACL verification unavailable on Windows for ${effectivePath}. Move the command to a path whose ACLs OpenClaw can verify; there is no provider-level bypass.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,3 +60,95 @@ export function withMockedWindowsPlatform<T>(run: () => T): T;
|
||||
export function withMockedWindowsPlatform<T>(run: () => T | Promise<T>): T | Promise<T> {
|
||||
return withMockedPlatform("win32", run);
|
||||
}
|
||||
|
||||
const WINDOWS_ACL_ENV_KEYS = new Set([
|
||||
"fs_safe_native_mode",
|
||||
"openclaw_fs_safe_native_mode",
|
||||
"systemroot",
|
||||
"windir",
|
||||
]);
|
||||
const NODE_OPTIONS_ENV_KEY = "node_options";
|
||||
|
||||
function takeWindowsAclEnvSnapshot(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(([key]) => WINDOWS_ACL_ENV_KEYS.has(key.toLowerCase())),
|
||||
);
|
||||
}
|
||||
|
||||
function clearWindowsAclEnv(env: NodeJS.ProcessEnv): void {
|
||||
for (const key of Object.keys(env)) {
|
||||
if (WINDOWS_ACL_ENV_KEYS.has(key.toLowerCase())) {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forceFsSafeNativeFallback(env: NodeJS.ProcessEnv): void {
|
||||
for (const key of Object.keys(env)) {
|
||||
const normalized = key.toLowerCase();
|
||||
if (normalized === "fs_safe_native_mode" || normalized === "openclaw_fs_safe_native_mode") {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
env.FS_SAFE_NATIVE_MODE = "off";
|
||||
env.OPENCLAW_FS_SAFE_NATIVE_MODE = "off";
|
||||
}
|
||||
|
||||
function forceWindowsAclVerificationUnavailable(
|
||||
env: NodeJS.ProcessEnv,
|
||||
missingSystemRoot: string,
|
||||
): void {
|
||||
clearWindowsAclEnv(env);
|
||||
// Disable the optional native backend and make both Windows ACL tool paths
|
||||
// unavailable, so permission checks exercise the real fail-closed result.
|
||||
forceFsSafeNativeFallback(env);
|
||||
env.SystemRoot = missingSystemRoot;
|
||||
env.WINDIR = missingSystemRoot;
|
||||
}
|
||||
|
||||
export function forceNativeWindowsAclToolsUnavailable(
|
||||
env: NodeJS.ProcessEnv,
|
||||
preloadUrl: string,
|
||||
): void {
|
||||
forceFsSafeNativeFallback(env);
|
||||
let existingNodeOptions: string | undefined;
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key.toLowerCase() !== NODE_OPTIONS_ENV_KEY) {
|
||||
continue;
|
||||
}
|
||||
existingNodeOptions ??= env[key];
|
||||
delete env[key];
|
||||
}
|
||||
env.NODE_OPTIONS = [existingNodeOptions, `--import=${preloadUrl}`].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function withMockedWindowsAclVerificationUnavailable<T>(
|
||||
missingSystemRoot: string,
|
||||
run: () => Promise<T>,
|
||||
): Promise<T>;
|
||||
export function withMockedWindowsAclVerificationUnavailable<T>(
|
||||
missingSystemRoot: string,
|
||||
run: () => T,
|
||||
): T;
|
||||
export function withMockedWindowsAclVerificationUnavailable<T>(
|
||||
missingSystemRoot: string,
|
||||
run: () => T | Promise<T>,
|
||||
): T | Promise<T> {
|
||||
const snapshot = takeWindowsAclEnvSnapshot(process.env);
|
||||
forceWindowsAclVerificationUnavailable(process.env, missingSystemRoot);
|
||||
const restore = () => {
|
||||
clearWindowsAclEnv(process.env);
|
||||
Object.assign(process.env, snapshot);
|
||||
};
|
||||
try {
|
||||
const result = withMockedWindowsPlatform(run);
|
||||
if (isPromiseLike(result)) {
|
||||
return result.finally(restore);
|
||||
}
|
||||
restore();
|
||||
return result;
|
||||
} catch (error) {
|
||||
restore();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// QA Lab product proof for doctor gateway auth and SecretRef behavior.
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { stripAnsiSequences } from "../../../../packages/terminal-core/src/ansi.js";
|
||||
import type { OpenClawConfig } from "../../../../src/config/types.openclaw.js";
|
||||
import { withSecureTestNodeCommand } from "../../../../src/secrets/test-node-command.test-support.js";
|
||||
import { forceNativeWindowsAclToolsUnavailable } from "../../../../src/test-utils/vitest-spies.js";
|
||||
import {
|
||||
createOpenClawTestInstance,
|
||||
type OpenClawTestInstance,
|
||||
@@ -12,6 +16,7 @@ import {
|
||||
|
||||
let instance: OpenClawTestInstance | undefined;
|
||||
type GatewayToken = NonNullable<NonNullable<OpenClawConfig["gateway"]>["auth"]>["token"];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
afterEach(async () => {
|
||||
await instance?.cleanup();
|
||||
@@ -23,7 +28,7 @@ function outputOf(result: { stderr: string; stdout: string }): string {
|
||||
}
|
||||
|
||||
function normalizedOutputOf(result: { stderr: string; stdout: string }): string {
|
||||
return stripAnsiSequences(outputOf(result)).replace(/\s+/g, " ").trim();
|
||||
return stripAnsiSequences(outputOf(result)).replaceAll("│", " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
async function writeConfig(config: OpenClawConfig): Promise<void> {
|
||||
@@ -45,6 +50,24 @@ function localGatewayConfig(token?: GatewayToken): OpenClawConfig {
|
||||
};
|
||||
}
|
||||
|
||||
async function expectAclFixturePreservesExecFileContract(preloadUrl: string): Promise<void> {
|
||||
const probe = [
|
||||
'import { execFile } from "node:child_process";',
|
||||
'import { promisify } from "node:util";',
|
||||
'const promise = promisify(execFile)(process.execPath, ["--version"], { encoding: "utf8" });',
|
||||
'if (!promise.child || typeof promise.child.kill !== "function") process.exit(2);',
|
||||
"const result = await promise;",
|
||||
'if (!result || typeof result.stdout !== "string" || typeof result.stderr !== "string") process.exit(3);',
|
||||
'process.stdout.write("ok");',
|
||||
].join("");
|
||||
const result = await execFileAsync(
|
||||
process.execPath,
|
||||
[`--import=${preloadUrl}`, "--input-type=module", "--eval", probe],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
expect(result).toEqual({ stdout: "ok", stderr: "" });
|
||||
}
|
||||
|
||||
describe("doctor auth and SecretRef product proof", () => {
|
||||
it(
|
||||
"preserves SecretRef ownership while proving resolution, fallback, exec gating, and token generation",
|
||||
@@ -103,6 +126,50 @@ describe("doctor auth and SecretRef product proof", () => {
|
||||
};
|
||||
expect(unresolvedConfig.gateway?.auth?.token).toEqual(unresolvedRef);
|
||||
|
||||
const aclFixtureUrl = pathToFileURL(
|
||||
path.resolve("test/fixtures/windows-acl-tools-unavailable.mjs"),
|
||||
).href;
|
||||
await expectAclFixturePreservesExecFileContract(aclFixtureUrl);
|
||||
if (process.platform === "win32") {
|
||||
forceNativeWindowsAclToolsUnavailable(instance.env, aclFixtureUrl);
|
||||
}
|
||||
|
||||
const filePath = path.join(instance.stateDir, "doctor-file-secretref.json");
|
||||
const fileSecret = "qa-file-token";
|
||||
await fs.writeFile(filePath, JSON.stringify({ gateway: { token: fileSecret } }), {
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeConfig({
|
||||
...localGatewayConfig({
|
||||
source: "file",
|
||||
provider: "filemain",
|
||||
id: "/gateway/token",
|
||||
}),
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const fileResult = await instance.cli(
|
||||
["doctor", "--non-interactive", "--no-workspace-suggestions"],
|
||||
{ timeoutMs: 120_000 },
|
||||
);
|
||||
expect(fileResult.code).toBe(0);
|
||||
const fileOutput = normalizedOutputOf(fileResult);
|
||||
if (process.platform === "win32") {
|
||||
expect(fileOutput).toMatch(
|
||||
/Gateway token SecretRef could not be resolved: .*Windows path security could not be verified\. Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify\./,
|
||||
);
|
||||
expect(fileOutput).not.toContain(filePath);
|
||||
} else {
|
||||
expect(fileOutput).not.toContain("Gateway token SecretRef could not be resolved");
|
||||
}
|
||||
expect(fileOutput).not.toContain(fileSecret);
|
||||
|
||||
const execMarker = path.join(instance.stateDir, "doctor-exec-secretref.marker");
|
||||
const execScript = [
|
||||
"const fs = require('node:fs');",
|
||||
@@ -146,8 +213,10 @@ describe("doctor auth and SecretRef product proof", () => {
|
||||
const execAllowedOutput = normalizedOutputOf(execAllowed);
|
||||
if (process.platform === "win32") {
|
||||
expect(execAllowedOutput).toMatch(
|
||||
/Gateway token SecretRef could not be resolved: .*ACL verification unavailable on Windows/,
|
||||
/Gateway token SecretRef could not be resolved: .*Windows path security could not be verified\. Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify\./,
|
||||
);
|
||||
expect(execAllowedOutput).not.toContain(command);
|
||||
expect(execAllowedOutput).not.toContain(execMarker);
|
||||
await expect(fs.access(execMarker)).rejects.toThrow();
|
||||
} else {
|
||||
await expect(fs.readFile(execMarker, "utf8")).resolves.toBe("executed");
|
||||
@@ -183,6 +252,8 @@ describe("doctor auth and SecretRef product proof", () => {
|
||||
execRefGated: true,
|
||||
execRefAllowed: process.platform !== "win32",
|
||||
execRefWindowsAclBlocked: process.platform === "win32",
|
||||
fileRefAllowed: process.platform !== "win32",
|
||||
fileRefWindowsAclBlocked: process.platform === "win32",
|
||||
generatedTokenPersisted: true,
|
||||
})}`,
|
||||
);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import childProcess from "node:child_process";
|
||||
import { syncBuiltinESMExports } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const WINDOWS_ACL_TOOLS = new Set(["icacls.exe", "powershell.exe", "whoami.exe"]);
|
||||
const realExecFile = childProcess.execFile;
|
||||
const missingToolPath = fileURLToPath(new URL("missing-windows-acl-tool.exe", import.meta.url));
|
||||
|
||||
function fixtureExecFile(command, ...args) {
|
||||
if (!WINDOWS_ACL_TOOLS.has(path.win32.basename(String(command)).toLowerCase())) {
|
||||
return realExecFile.call(this, command, ...args);
|
||||
}
|
||||
return realExecFile.call(this, missingToolPath, ...args);
|
||||
}
|
||||
|
||||
Object.defineProperty(fixtureExecFile, promisify.custom, {
|
||||
value: function promisifiedExecFile(...args) {
|
||||
let child;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
child = fixtureExecFile(...args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
error.stdout = stdout;
|
||||
error.stderr = stderr;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
promise.child = child;
|
||||
return promise;
|
||||
},
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
childProcess.execFile = fixtureExecFile;
|
||||
syncBuiltinESMExports();
|
||||
@@ -197,6 +197,18 @@ describe("package scripts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("runs Doctor SecretRef ACL coverage in Windows CI", () => {
|
||||
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
|
||||
"test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the Doctor managed-service SecretRef renderer in Windows CI", () => {
|
||||
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
|
||||
"src/commands/doctor-gateway-auth-token.windows.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs legacy session importer atomicity coverage in Windows CI", () => {
|
||||
expect(readPackageJson().scripts["test:windows:ci"]).toContain(
|
||||
"src/infra/state-migrations.legacy-session-store.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user