mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
e93e9afcd6
* detect coding agent environments Co-authored-by: Cursor <cursoragent@cursor.com> * consolidate environment detection helpers Co-authored-by: Cursor <cursoragent@cursor.com> * fix prompt env test cleanup Co-authored-by: Cursor <cursoragent@cursor.com> * centralize environment test signals Co-authored-by: Cursor <cursoragent@cursor.com> * fix environment helper formatting Co-authored-by: Cursor <cursoragent@cursor.com> * Fix case-sensitive AGENT matching, swap eval order, add missing test coverage Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES,
|
|
CODING_AGENT_ENVIRONMENT_VARIABLES,
|
|
} from "../src/cli/utils/is-ci-environment.js";
|
|
import { NON_INTERACTIVE_ENVIRONMENT_VARIABLES } from "../src/cli/utils/is-non-interactive-environment.js";
|
|
import { isSpinnerInteractive } from "../src/cli/utils/is-spinner-interactive.js";
|
|
|
|
interface StreamStubHandle {
|
|
restore: () => void;
|
|
}
|
|
|
|
const stubStream = (
|
|
stream: NodeJS.WriteStream,
|
|
overrides: Partial<{ isTTY: boolean; columns: number }>,
|
|
): StreamStubHandle => {
|
|
const previousIsTty = Object.getOwnPropertyDescriptor(stream, "isTTY");
|
|
const previousColumns = Object.getOwnPropertyDescriptor(stream, "columns");
|
|
|
|
if ("isTTY" in overrides) {
|
|
Object.defineProperty(stream, "isTTY", {
|
|
value: overrides.isTTY,
|
|
configurable: true,
|
|
});
|
|
}
|
|
if ("columns" in overrides) {
|
|
Object.defineProperty(stream, "columns", {
|
|
value: overrides.columns,
|
|
configurable: true,
|
|
});
|
|
}
|
|
|
|
return {
|
|
restore: () => {
|
|
if (previousIsTty) {
|
|
Object.defineProperty(stream, "isTTY", previousIsTty);
|
|
} else {
|
|
delete (stream as unknown as { isTTY?: boolean }).isTTY;
|
|
}
|
|
if (previousColumns) {
|
|
Object.defineProperty(stream, "columns", previousColumns);
|
|
} else {
|
|
delete (stream as unknown as { columns?: number }).columns;
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
...NON_INTERACTIVE_ENVIRONMENT_VARIABLES,
|
|
...CODING_AGENT_ENVIRONMENT_VARIABLES,
|
|
...CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES,
|
|
"TERM",
|
|
] as const;
|
|
|
|
describe("isSpinnerInteractive", () => {
|
|
let savedEnv: Record<string, string | undefined>;
|
|
let stderrHandle: StreamStubHandle;
|
|
let stdoutHandle: StreamStubHandle;
|
|
|
|
beforeEach(() => {
|
|
savedEnv = {};
|
|
for (const envVarName of NON_INTERACTIVE_ENV_VARS) {
|
|
savedEnv[envVarName] = process.env[envVarName];
|
|
delete process.env[envVarName];
|
|
}
|
|
stderrHandle = stubStream(process.stderr, { isTTY: true, columns: 80 });
|
|
stdoutHandle = stubStream(process.stdout, { isTTY: true, columns: 80 });
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const envVarName of NON_INTERACTIVE_ENV_VARS) {
|
|
const previousValue = savedEnv[envVarName];
|
|
if (previousValue === undefined) {
|
|
delete process.env[envVarName];
|
|
} else {
|
|
process.env[envVarName] = previousValue;
|
|
}
|
|
}
|
|
stdoutHandle.restore();
|
|
stderrHandle.restore();
|
|
});
|
|
|
|
it("returns true on a fully interactive stderr TTY with sensible columns", () => {
|
|
expect(isSpinnerInteractive()).toBe(true);
|
|
});
|
|
|
|
it("returns false when stderr is not a TTY", () => {
|
|
stderrHandle.restore();
|
|
stderrHandle = stubStream(process.stderr, { isTTY: false, columns: 80 });
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
// Regression guard for #293: under `script(1)` and Git pre-push hooks
|
|
// the stream ora renders to inherits a TTY but `columns` is reported
|
|
// as 0/undefined. Without this check, ora's render loop computes
|
|
// `Math.ceil(width / 0) === Infinity` lines to clear and emits
|
|
// unbounded cursor-up + erase-line escapes (99% CPU, never returns).
|
|
it("returns false when columns is 0 (e.g. under `script(1)` / Git hooks)", () => {
|
|
stderrHandle.restore();
|
|
stderrHandle = stubStream(process.stderr, { isTTY: true, columns: 0 });
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
it("returns false when columns is undefined", () => {
|
|
stderrHandle.restore();
|
|
stderrHandle = stubStream(process.stderr, {
|
|
isTTY: true,
|
|
columns: undefined as unknown as number,
|
|
});
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
// Regression guard for the React Review P1 on PR #296: the guard must
|
|
// consult the same stream ora renders to (stderr by default), not
|
|
// stdout. Otherwise the hang reappears any time stderr is the TTY with
|
|
// 0/undefined columns while stdout is healthy (a Git pre-push hook
|
|
// where stdout is piped to the hook runner but stderr inherits the
|
|
// parent TTY).
|
|
it("returns false based on stderr, not stdout, when called with no argument", () => {
|
|
stderrHandle.restore();
|
|
stderrHandle = stubStream(process.stderr, { isTTY: true, columns: 0 });
|
|
// stdout is still healthy in beforeEach.
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
it("accepts an explicit stream argument and checks that one", () => {
|
|
stderrHandle.restore();
|
|
stderrHandle = stubStream(process.stderr, { isTTY: false, columns: 80 });
|
|
// stdout is healthy, so when we explicitly pass it we should pass.
|
|
expect(isSpinnerInteractive(process.stdout)).toBe(true);
|
|
});
|
|
|
|
it("returns false when TERM is `dumb`", () => {
|
|
process.env.TERM = "dumb";
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
it("returns false when CI env var is set, even on a TTY", () => {
|
|
process.env.CI = "true";
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
it("returns false when CURSOR_AGENT env var is set", () => {
|
|
process.env.CURSOR_AGENT = "1";
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
|
|
// Regression guard for #293's primary trigger: lefthook/husky/etc.
|
|
// hook scripts run with `GIT_DIR` set by git itself (per
|
|
// `git-hooks(5)`). This catches every git-hook manager at once
|
|
// without needing per-tool env vars.
|
|
it("returns false when GIT_DIR is set (git hook execution)", () => {
|
|
process.env.GIT_DIR = "/repo/.git";
|
|
expect(isSpinnerInteractive()).toBe(false);
|
|
});
|
|
});
|