mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 22:32:33 +00:00
fix(scripts): harden Windows QA runners
This commit is contained in:
@@ -74,6 +74,8 @@ Docs: https://docs.openclaw.ai
|
||||
- OpenAI/images: route Codex API-key image generation through the native OpenAI Images API instead of the Codex OAuth streaming backend, avoiding 401s from valid API keys.
|
||||
- Agents/OpenAI completions: omit empty tool payload fields for proxy-like OpenAI-compatible endpoints so strict vLLM-style servers accept tool-free turns. (#85835) Thanks @rendrag-git.
|
||||
- Sandbox: keep workspace skill mounts read-only for remote container-cwd file operations and reject symlinked skill roots before creating protected overlays. (#85591) Thanks @jason-allen-oneal.
|
||||
- Scripts/Windows: route remaining QA, release, profile, and live-media `pnpm` launches through the managed runner so native Windows avoids brittle `.cmd` execution and shell-argv warnings.
|
||||
- Release: align generated config/API baselines and the meeting-notes plugin version so release preflight stays green on native Windows.
|
||||
- Checks/Windows: route full `pnpm check` stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too.
|
||||
- Checks/Windows: run managed child commands through explicit `cmd.exe` wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks.
|
||||
- Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
5482b1a125a5c41856f6f49dfd70e2efe9e52a7cc0e2d4c24a56d99adfeda6be config-baseline.json
|
||||
3d686075da4d4f6c6319c3247e93f486a6c48314a28a2961cd4acab7f3fa5389 config-baseline.core.json
|
||||
11839c7a1b858c66075156f0e203aa8367cd8321047684679a18e18b7c8fe1f7 config-baseline.channel.json
|
||||
5c214ab364011fd95735755f9fa4298aa4de8ad81144ae8dd08d969bb7ba318b config-baseline.plugin.json
|
||||
e0fee2c8da83aa14f45b9e00bde313af1d7e191c3b6ab892efea5799d9a1e9fe config-baseline.json
|
||||
c746fff7ae66db26fbad9add82c2f4cb23570cb18582b48c0b7061046a7e07fa config-baseline.core.json
|
||||
859b021f65400df22c95ae55b074cf26c83d3a0bfadb3fceeaca522f6ea391ae config-baseline.channel.json
|
||||
74441e331aabb3026784c148d4ee5ce3f489a15ed87ffd9b7ba0c5e2a7bc93be config-baseline.plugin.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/meeting-notes",
|
||||
"version": "2026.5.21",
|
||||
"version": "2026.5.22",
|
||||
"private": true,
|
||||
"description": "OpenClaw meeting notes plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { collectGatewayCpuObservations } from "./lib/plugin-gateway-gauntlet.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
|
||||
|
||||
const DEFAULT_STARTUP_CASES = ["default", "oneInternalHook", "allInternalHooks"];
|
||||
const DEFAULT_QA_SCENARIOS = [
|
||||
@@ -136,20 +137,26 @@ function readJsonIfExists(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function runStep(name, command, args) {
|
||||
function runStep(name, command, args, options = {}) {
|
||||
console.error(`[gateway-cpu] start ${name}`);
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
...options,
|
||||
});
|
||||
const status = result.status ?? (result.signal ? 1 : 0);
|
||||
console.error(`[gateway-cpu] ${status === 0 ? "pass" : "fail"} ${name}`);
|
||||
return { name, status, signal: result.signal ?? null };
|
||||
}
|
||||
|
||||
function pnpmCommand() {
|
||||
return process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
function pnpmCommand(args) {
|
||||
return createPnpmRunnerSpawnSpec({
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
pnpmArgs: args,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
function toRepoRelativePath(absolutePath) {
|
||||
@@ -187,19 +194,20 @@ async function main() {
|
||||
}
|
||||
|
||||
if (!options.skipQa) {
|
||||
const qaCommand = pnpmCommand([
|
||||
"openclaw",
|
||||
"qa",
|
||||
"suite",
|
||||
"--provider-mode",
|
||||
"mock-openai",
|
||||
"--concurrency",
|
||||
"1",
|
||||
"--output-dir",
|
||||
qaOutputArg,
|
||||
...options.qaScenarios.flatMap((id) => ["--scenario", id]),
|
||||
]);
|
||||
steps.push(
|
||||
runStep("qa suite", pnpmCommand(), [
|
||||
"openclaw",
|
||||
"qa",
|
||||
"suite",
|
||||
"--provider-mode",
|
||||
"mock-openai",
|
||||
"--concurrency",
|
||||
"1",
|
||||
"--output-dir",
|
||||
qaOutputArg,
|
||||
...options.qaScenarios.flatMap((id) => ["--scenario", id]),
|
||||
]),
|
||||
runStep("qa suite", qaCommand.command, qaCommand.args, qaCommand.options),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
discoverBundledPluginManifests,
|
||||
selectPluginEntries,
|
||||
} from "./lib/plugin-gateway-gauntlet.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
|
||||
|
||||
const DEFAULT_QA_SCENARIOS = [
|
||||
"channel-chat-baseline",
|
||||
@@ -231,8 +232,13 @@ function parsePositiveNumber(raw, label) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function pnpmCommand() {
|
||||
return process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
function pnpmCommand(args, { cwd, env }) {
|
||||
return createPnpmRunnerSpawnSpec({
|
||||
cwd,
|
||||
env,
|
||||
pnpmArgs: args,
|
||||
stdio: "pipe",
|
||||
});
|
||||
}
|
||||
|
||||
function openclawCommand(repoRoot, args) {
|
||||
@@ -361,6 +367,7 @@ function runMeasuredCommand(params) {
|
||||
encoding: "utf8",
|
||||
timeout: params.timeoutMs,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
...(mode === "none" ? (params.spawnOptions ?? {}) : {}),
|
||||
});
|
||||
const wallMs = performance.now() - started;
|
||||
const status = result.status ?? (result.signal ? 1 : 0);
|
||||
@@ -509,13 +516,16 @@ async function main() {
|
||||
const rows = [];
|
||||
if (!options.skipPrebuild && (selectedPlugins.length > 0 || !options.skipQa)) {
|
||||
process.stderr.write("[plugin-gauntlet] prebuild\n");
|
||||
const prebuildEnv = buildGauntletPrebuildEnv(env, { includePrivateQa: !options.skipQa });
|
||||
const prebuildCommand = pnpmCommand(["build"], { cwd: repoRoot, env: prebuildEnv });
|
||||
rows.push(
|
||||
runMeasuredCommand({
|
||||
cwd: repoRoot,
|
||||
env: buildGauntletPrebuildEnv(env, { includePrivateQa: !options.skipQa }),
|
||||
env: prebuildEnv,
|
||||
logDir: path.join(options.outputDir, "logs", "prebuild"),
|
||||
command: pnpmCommand(),
|
||||
args: ["build"],
|
||||
command: prebuildCommand.command,
|
||||
args: prebuildCommand.args,
|
||||
spawnOptions: prebuildCommand.options,
|
||||
label: "prebuild",
|
||||
phase: "prebuild",
|
||||
timeoutMs: options.buildTimeoutMs,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "..");
|
||||
const tsgoPath = path.join(repoRoot, "node_modules", ".bin", "tsgo");
|
||||
@@ -23,11 +24,16 @@ function normalizeFilePath(filePath) {
|
||||
}
|
||||
|
||||
function listGraphFiles(graph) {
|
||||
const result = spawnSync(tsgoPath, ["-p", graph.config, "--pretty", "false", "--listFilesOnly"], {
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: ["-p", graph.config, "--pretty", "false", "--listFilesOnly"],
|
||||
bin: tsgoPath,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
shell: process.platform === "win32",
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
|
||||
@@ -431,7 +431,7 @@ async function waitForGatewayReady(child, port, logPath) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
if (fs.existsSync(logPath) && fs.readFileSync(logPath, "utf8").includes("[gateway] ready")) {
|
||||
return;
|
||||
lastError = `${lastError}; gateway log reported ready before HTTP readiness`;
|
||||
}
|
||||
await delay(250);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalOxlintPolicy,
|
||||
} from "./local-heavy-check-runtime.mjs";
|
||||
import { createManagedCommandInvocation } from "./managed-child-process.mjs";
|
||||
|
||||
export function runExtensionOxlint(params) {
|
||||
const repoRoot = process.cwd();
|
||||
@@ -39,10 +40,16 @@ export function runExtensionOxlint(params) {
|
||||
|
||||
const baseArgs = ["-c", tempConfigPath, ...process.argv.slice(2), ...extensionFiles];
|
||||
const { args: finalArgs, env } = applyLocalOxlintPolicy(baseArgs, process.env);
|
||||
const result = spawnSync(oxlintPath, finalArgs, {
|
||||
const oxlint = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: oxlintPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(oxlint.command, oxlint.args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
shell: oxlint.shell,
|
||||
windowsVerbatimArguments: oxlint.windowsVerbatimArguments,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { spawnPnpmRunner } from "../pnpm-runner.mjs";
|
||||
import {
|
||||
installVitestProcessGroupCleanup,
|
||||
shouldUseDetachedVitestProcessGroup,
|
||||
@@ -9,21 +9,24 @@ import {
|
||||
const scriptFile = fileURLToPath(import.meta.url);
|
||||
const scriptDir = path.dirname(scriptFile);
|
||||
const repoRoot = path.resolve(scriptDir, "../..");
|
||||
const pnpm = "pnpm";
|
||||
|
||||
export async function runVitestBatch(params) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
pnpm,
|
||||
["exec", "vitest", "run", "--config", params.config, ...params.targets, ...params.args],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
detached: shouldUseDetachedVitestProcessGroup(),
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
env: params.env,
|
||||
},
|
||||
);
|
||||
const child = spawnPnpmRunner({
|
||||
cwd: repoRoot,
|
||||
detached: shouldUseDetachedVitestProcessGroup(),
|
||||
env: params.env,
|
||||
pnpmArgs: [
|
||||
"exec",
|
||||
"vitest",
|
||||
"run",
|
||||
"--config",
|
||||
params.config,
|
||||
...params.targets,
|
||||
...params.args,
|
||||
],
|
||||
stdio: "inherit",
|
||||
});
|
||||
const teardownChildCleanup = installVitestProcessGroupCleanup({ child });
|
||||
|
||||
child.on("error", (error) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnSync, type SpawnSyncOptions } from "node:child_process";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
|
||||
const requiredPreparedPathGroups = [
|
||||
["dist/index.js", "dist/index.mjs"],
|
||||
["dist/control-ui/index.html"],
|
||||
@@ -90,10 +91,11 @@ function ensurePreparedArtifacts(): void {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run(command: string, args: string[]): void {
|
||||
function run(command: string, args: string[], options: SpawnSyncOptions = {}): void {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
...options,
|
||||
});
|
||||
if (result.status === 0) {
|
||||
return;
|
||||
@@ -101,6 +103,15 @@ function run(command: string, args: string[]): void {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
function runPnpm(args: string[]): void {
|
||||
const command = createPnpmRunnerSpawnSpec({
|
||||
env: process.env,
|
||||
pnpmArgs: args,
|
||||
stdio: "inherit",
|
||||
});
|
||||
run(command.command, command.args, command.options);
|
||||
}
|
||||
|
||||
function runBuildSmoke(): void {
|
||||
run(process.execPath, ["scripts/test-built-bundled-channel-entry-smoke.mjs"]);
|
||||
}
|
||||
@@ -110,9 +121,8 @@ async function writeDistInventory(): Promise<void> {
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
run(pnpmCommand, ["build"]);
|
||||
run(pnpmCommand, ["ui:build"]);
|
||||
runPnpm(["build"]);
|
||||
runPnpm(["ui:build"]);
|
||||
ensurePreparedArtifacts();
|
||||
await writeDistInventory();
|
||||
runBuildSmoke();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
applyLocalTsgoPolicy,
|
||||
shouldAcquireLocalHeavyCheckLockForTsgo,
|
||||
} from "./lib/local-heavy-check-runtime.mjs";
|
||||
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "..");
|
||||
const artifactRoot = path.resolve(repoRoot, ".artifacts/tsgo-profile");
|
||||
@@ -139,12 +140,18 @@ function runTsgo(label, args, params = {}) {
|
||||
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const result = spawnSync(tsgoPath, finalArgs, {
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
|
||||
shell: process.platform === "win32",
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const stdout = result.stdout ?? "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mjs";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const fix = args.has("--fix");
|
||||
@@ -9,8 +9,6 @@ if (fix && args.has("--check")) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
|
||||
const fixCommands = [
|
||||
{ name: "plugin versions", args: ["plugins:sync"] },
|
||||
{ name: "plugin inventory", args: ["plugins:inventory:gen"] },
|
||||
@@ -77,19 +75,15 @@ async function runAll(commands) {
|
||||
|
||||
async function runCommand(command) {
|
||||
console.log(`\n[release-preflight] ${command.name}: pnpm ${command.args.join(" ")}`);
|
||||
const child = spawn(pnpm, command.args, {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
});
|
||||
return await new Promise((resolve) => {
|
||||
child.once("error", (error) => {
|
||||
console.error(error);
|
||||
resolve(1);
|
||||
try {
|
||||
return await runManagedCommand({
|
||||
args: command.args,
|
||||
bin: "pnpm",
|
||||
});
|
||||
child.once("close", (status) => {
|
||||
resolve(status ?? 1);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function printFailures(title, failures) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "./lib/error-format.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
|
||||
|
||||
export function parseArgs(argv) {
|
||||
const args = {
|
||||
@@ -74,6 +75,25 @@ export function buildVitestProfileCommand({ mode, outputDir }) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildVitestProfileSpawnSpec(plan, runnerOptions = {}) {
|
||||
if (plan.command === "pnpm") {
|
||||
return createPnpmRunnerSpawnSpec({
|
||||
...runnerOptions,
|
||||
env: runnerOptions.env ?? process.env,
|
||||
pnpmArgs: plan.args,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
return {
|
||||
args: plan.args,
|
||||
command: plan.command,
|
||||
options: {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const parsed = parseArgs(process.argv.slice(2));
|
||||
const outputDir = resolveVitestProfileDir(parsed);
|
||||
@@ -86,11 +106,8 @@ function main() {
|
||||
|
||||
console.log(`[run-vitest-profile] writing ${parsed.mode} profiles to ${outputDir}`);
|
||||
|
||||
const result = spawnSync(plan.command, plan.args, {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32" && plan.command === "pnpm",
|
||||
env: process.env,
|
||||
});
|
||||
const spawnSpec = buildVitestProfileSpawnSpec(plan);
|
||||
const result = spawnSync(spawnSpec.command, spawnSpec.args, spawnSpec.options);
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { collectProviderApiKeys } from "../src/agents/live-auth-keys.js";
|
||||
@@ -94,19 +94,10 @@ export type SuiteRunPlan = {
|
||||
};
|
||||
|
||||
function spawnLivePnpm(params: { pnpmArgs: string[]; env: NodeJS.ProcessEnv }): ChildProcess {
|
||||
const npmExecPath = process.env.npm_execpath?.trim();
|
||||
if (npmExecPath) {
|
||||
return spawn(process.execPath, [npmExecPath, ...params.pnpmArgs], {
|
||||
stdio: "inherit",
|
||||
env: params.env,
|
||||
shell: false,
|
||||
});
|
||||
}
|
||||
|
||||
return spawn(process.platform === "win32" ? "pnpm.cmd" : "pnpm", params.pnpmArgs, {
|
||||
return _spawnPnpmRunner({
|
||||
pnpmArgs: params.pnpmArgs,
|
||||
stdio: "inherit",
|
||||
env: params.env,
|
||||
shell: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildVitestProfileSpawnSpec,
|
||||
buildVitestProfileCommand,
|
||||
parseArgs,
|
||||
resolveVitestProfileDir,
|
||||
@@ -58,6 +59,26 @@ describe("scripts/run-vitest-profile", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the Windows-safe pnpm fallback for runner profiling", () => {
|
||||
const spawnSpec = buildVitestProfileSpawnSpec(
|
||||
{
|
||||
command: "pnpm",
|
||||
args: ["vitest", "run"],
|
||||
},
|
||||
{
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
env: {},
|
||||
npmExecPath: "",
|
||||
platform: "win32",
|
||||
},
|
||||
);
|
||||
|
||||
expect(spawnSpec.options.shell).toBe(false);
|
||||
expect(spawnSpec.command).toBe("C:\\Windows\\System32\\cmd.exe");
|
||||
expect(spawnSpec.options.windowsVerbatimArguments).toBe(true);
|
||||
expect(spawnSpec.args).toEqual(["/d", "/s", "/c", "pnpm.cmd vitest run"]);
|
||||
});
|
||||
|
||||
it("parses mode and explicit output dir", () => {
|
||||
expect(parseArgs(["runner", "--output-dir", "/tmp/out"])).toEqual({
|
||||
mode: "runner",
|
||||
|
||||
Reference in New Issue
Block a user