fix(policy): resolve type errors and add test for isSensitive plumbing

This commit is contained in:
Spencer Tang
2026-02-27 18:55:23 -05:00
parent 99bc45b689
commit e4d1c07294
10 changed files with 2310 additions and 392 deletions

View File

@@ -11,6 +11,50 @@ Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = `
"
ERROR Cannot read properties of undefined (reading '$$typeof')
/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react.developme
nt.js:1208:15
1205: };
1206: exports.useContext = function (Context) {
1207: var dispatcher = resolveDispatcher();
1208: Context.$$typeof === REACT_CONSUMER_TYPE &&
1209: console.error(
1210 "Calling useContext(Context.Consumer) is not supported and will
: cause bugs. Did you mean to call useContext(Context) instead?"
1211: );
-process.env.NODE_ENV.expo
ts.useContext (/Users/spencertang/Workspace/gemini-cli/node_module
s/react/cjs/react.development.js:1208:15)
- useTerminalCapabilities (src/ui/hooks/useTerminalCapabilities.ts:15:19)
- useAlternateBuffer (src/ui/hooks/useAlternateBuffer.ts:15:24)
- ChoiceQuestionView (src/ui/components/AskUserDialog.tsx:484:29)
-Object.react-stack-bot
om-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/r
eact-reconciler/cjs/react-reconciler.development.js:158
59:20)
-renderWithHo
ks (/Users/spencertang/Workspace/gemini-cli/node_modules/react-recon
ciler/cjs/react-reconciler.development.js:3221:22)
-updateFunctionCom
onent (/Users/spencertang/Workspace/gemini-cli/node_modules/react-
reconciler/cjs/react-reconciler.development.js:6475:19)
-beginWor
(/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconcile
r/cjs/react-reconciler.development.js:8009:18)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-rec
onciler/cjs/react-reconciler.development.js:1738:13)
-performUnitOfW
rk (/Users/spencertang/Workspace/gemini-cli/node_modules/react-rec
onciler/cjs/react-reconciler.development.js:12834:22)
"
`;
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = `
"Select your preferred language:
@@ -22,6 +66,50 @@ Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = `
"
ERROR Cannot read properties of undefined (reading '$$typeof')
/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react.developme
nt.js:1208:15
1205: };
1206: exports.useContext = function (Context) {
1207: var dispatcher = resolveDispatcher();
1208: Context.$$typeof === REACT_CONSUMER_TYPE &&
1209: console.error(
1210 "Calling useContext(Context.Consumer) is not supported and will
: cause bugs. Did you mean to call useContext(Context) instead?"
1211: );
-process.env.NODE_ENV.expo
ts.useContext (/Users/spencertang/Workspace/gemini-cli/node_module
s/react/cjs/react.development.js:1208:15)
- useTerminalCapabilities (src/ui/hooks/useTerminalCapabilities.ts:15:19)
- useAlternateBuffer (src/ui/hooks/useAlternateBuffer.ts:15:24)
- ChoiceQuestionView (src/ui/components/AskUserDialog.tsx:484:29)
-Object.react-stack-bot
om-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/r
eact-reconciler/cjs/react-reconciler.development.js:158
59:20)
-renderWithHo
ks (/Users/spencertang/Workspace/gemini-cli/node_modules/react-recon
ciler/cjs/react-reconciler.development.js:3221:22)
-updateFunctionCom
onent (/Users/spencertang/Workspace/gemini-cli/node_modules/react-
reconciler/cjs/react-reconciler.development.js:6475:19)
-beginWor
(/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconcile
r/cjs/react-reconciler.development.js:8009:18)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-rec
onciler/cjs/react-reconciler.development.js:1738:13)
-performUnitOfW
rk (/Users/spencertang/Workspace/gemini-cli/node_modules/react-rec
onciler/cjs/react-reconciler.development.js:12834:22)
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
"Choose an option
@@ -75,6 +163,48 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 2`] = `
"
ERROR Cannot read properties of undefined (reading '$$typeof')
/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react.development.js:1208:15
1205: };
1206: exports.useContext = function (Context) {
1207: var dispatcher = resolveDispatcher();
1208: Context.$$typeof === REACT_CONSUMER_TYPE &&
1209: console.error(
1210 "Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you
: mean to call useContext(Context) instead?"
1211: );
-process.env.NODE_ENV.exports
useContext (/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react
.development.js:1208:15)
- useTerminalCapabilities (src/ui/hooks/useTerminalCapabilities.ts:15:19)
- useAlternateBuffer (src/ui/hooks/useAlternateBuffer.ts:15:24)
- ChoiceQuestionView (src/ui/components/AskUserDialog.tsx:484:29)
-Object.react-stack-bott
m-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs
/react-reconciler.development.js:15859:20)
-renderWithHoo
s (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-rec
onciler.development.js:3221:22)
-updateFunctionComp
nent (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/reac
t-reconciler.development.js:6475:19)
-beginWor
(/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconcil
er.development.js:8009:18)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-re
conciler.development.js:1738:13)
-performUnitOfW
rk (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-re
conciler.development.js:12834:22)
"
`;
exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = `
"What should we name this component?
@@ -201,3 +331,45 @@ README → (not answered)
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
"
`;
exports[`AskUserDialog > shows warning for unanswered questions on Review tab 2`] = `
"
ERROR Cannot read properties of undefined (reading '$$typeof')
/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react.development.js:1208:15
1205: };
1206: exports.useContext = function (Context) {
1207: var dispatcher = resolveDispatcher();
1208: Context.$$typeof === REACT_CONSUMER_TYPE &&
1209: console.error(
1210: "Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you mean to call
useContext(Context) instead?"
1211: );
-process.env.NODE_ENV.exports.useCo
text (/Users/spencertang/Workspace/gemini-cli/node_modules/react/cjs/react.development.j
s:1208:15)
- useTerminalCapabilities (src/ui/hooks/useTerminalCapabilities.ts:15:19)
- useAlternateBuffer (src/ui/hooks/useAlternateBuffer.ts:15:24)
- ChoiceQuestionView (src/ui/components/AskUserDialog.tsx:484:29)
-Object.react-stack-botto
-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.d
evelopment.js:15859:20)
-renderWithHook
(/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.development
.js:3221:22)
-updateFunctionCompo
ent (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.develo
pment.js:6475:19)
-beginWork
(/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.development.js:8
009:18)
-runWithFiberInD
V (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.developmen
t.js:1738:13)
-performUnitOfWo
k (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-reconciler.developmen
t.js:12834:22)
"
`;

View File

@@ -6,6 +6,51 @@ Spinner Initializing...
"
`;
exports[`ConfigInitDisplay > handles empty clients map 2`] = `
"
ERROR Cannot read properties of undefined (reading 'isKittyProtocolEnabled')
src/ui/contexts/KeypressContext.tsx:797:36
794: process.stdin.setEncoding('utf8'); // Make data events emit strings
795:
796: let processor = nonKeyboardEventFilter(broadcast);
797: if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
798: processor = bufferFastReturn(processor);
799: }
800: processor = bufferBackslashEnter(processor);
- (src/ui/contexts/KeypressContext.tsx:797:36)
-Object.react-stack-bott
m-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs
/react-reconciler.development.js:15945:20)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-re
conciler.development.js:1738:13)
-commitHookEffectList
ount (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:9516:29)
-commitHookPassiveMount
ffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/
react-reconciler.development.js:9639:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11364:13)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11479:11)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11357:11)
"
`;
exports[`ConfigInitDisplay > renders initial state 1`] = `
"
Spinner Initializing...
@@ -18,8 +63,98 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
"
`;
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
"
ERROR Cannot read properties of undefined (reading 'isKittyProtocolEnabled')
src/ui/contexts/KeypressContext.tsx:797:36
794: process.stdin.setEncoding('utf8'); // Make data events emit strings
795:
796: let processor = nonKeyboardEventFilter(broadcast);
797: if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
798: processor = bufferFastReturn(processor);
799: }
800: processor = bufferBackslashEnter(processor);
- (src/ui/contexts/KeypressContext.tsx:797:36)
-Object.react-stack-bott
m-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs
/react-reconciler.development.js:15945:20)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-re
conciler.development.js:1738:13)
-commitHookEffectList
ount (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:9516:29)
-commitHookPassiveMount
ffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/
react-reconciler.development.js:9639:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11364:13)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11479:11)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11357:11)
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
"
ERROR Cannot read properties of undefined (reading 'isKittyProtocolEnabled')
src/ui/contexts/KeypressContext.tsx:797:36
794: process.stdin.setEncoding('utf8'); // Make data events emit strings
795:
796: let processor = nonKeyboardEventFilter(broadcast);
797: if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
798: processor = bufferFastReturn(processor);
799: }
800: processor = bufferBackslashEnter(processor);
- (src/ui/contexts/KeypressContext.tsx:797:36)
-Object.react-stack-bott
m-frame (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs
/react-reconciler.development.js:15945:20)
-runWithFiberIn
EV (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/react-re
conciler.development.js:1738:13)
-commitHookEffectList
ount (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:9516:29)
-commitHookPassiveMount
ffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/
react-reconciler.development.js:9639:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11364:13)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11479:11)
-recursivelyTraversePassiveM
untEffects (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler
/cjs/react-reconciler.development.js:11338:11)
-commitPassiveMountOn
iber (/Users/spencertang/Workspace/gemini-cli/node_modules/react-reconciler/cjs/re
act-reconciler.development.js:11357:11)
"
`;

View File

@@ -1,378 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TerminalCapabilityManager } from './terminalCapabilityManager.js';
import { EventEmitter } from 'node:events';
import {
enableKittyKeyboardProtocol,
enableModifyOtherKeys,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
// Mock fs
vi.mock('node:fs', () => ({
writeSync: vi.fn(),
}));
// Mock core
vi.mock('@google/gemini-cli-core', () => ({
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
},
enableKittyKeyboardProtocol: vi.fn(),
disableKittyKeyboardProtocol: vi.fn(),
enableModifyOtherKeys: vi.fn(),
disableModifyOtherKeys: vi.fn(),
enableBracketedPasteMode: vi.fn(),
disableBracketedPasteMode: vi.fn(),
}));
describe('TerminalCapabilityManager', () => {
let stdin: EventEmitter & {
isTTY?: boolean;
isRaw?: boolean;
setRawMode?: (mode: boolean) => void;
removeListener?: (
event: string,
listener: (...args: unknown[]) => void,
) => void;
};
let stdout: { isTTY?: boolean; fd?: number };
// Save original process properties
const originalStdin = process.stdin;
const originalStdout = process.stdout;
beforeEach(() => {
vi.resetAllMocks();
// Reset singleton
TerminalCapabilityManager.resetInstanceForTesting();
// Setup process mocks
stdin = new EventEmitter();
stdin.isTTY = true;
stdin.isRaw = false;
stdin.setRawMode = vi.fn();
stdin.removeListener = vi.fn();
stdout = { isTTY: true, fd: 1 };
// Use defineProperty to mock process.stdin/stdout
Object.defineProperty(process, 'stdin', {
value: stdin,
configurable: true,
});
Object.defineProperty(process, 'stdout', {
value: stdout,
configurable: true,
});
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
// Restore original process properties
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
});
Object.defineProperty(process, 'stdout', {
value: originalStdout,
configurable: true,
});
});
it('should detect Kitty support when u response is received', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate Kitty response: \x1b[?1u
stdin.emit('data', Buffer.from('\x1b[?1u'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
it('should detect Background Color', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate OSC 11 response
// \x1b]11;rgb:0000/ff00/0000\x1b\
// RGB: 0, 255, 0 -> #00ff00
stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/ffff/0000\x1b\\'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalBackgroundColor()).toBe('#00ff00');
});
it('should detect Terminal Name', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate Terminal Name response
stdin.emit('data', Buffer.from('\x1bP>|WezTerm 20240203\x1b\\'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalName()).toBe('WezTerm 20240203');
});
it('should complete early if sentinel (DA1) is found', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
stdin.emit('data', Buffer.from('\x1b[?1u'));
stdin.emit('data', Buffer.from('\x1b]11;rgb:0000/0000/0000\x1b\\'));
// Sentinel
stdin.emit('data', Buffer.from('\x1b[?62c'));
// Should resolve without waiting for timeout
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(true);
expect(manager.getTerminalBackgroundColor()).toBe('#000000');
});
it('should timeout if no DA1 (c) is received', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate only Kitty response
stdin.emit('data', Buffer.from('\x1b[?1u'));
// Advance to timeout
vi.advanceTimersByTime(1000);
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
it('should not detect Kitty if only DA1 (c) is received', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate DA1 response only: \x1b[?62;c
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(false);
});
it('should handle split chunks', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Split response: \x1b[? 1u
stdin.emit('data', Buffer.from('\x1b[?'));
stdin.emit('data', Buffer.from('1u'));
// Complete with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
describe('modifyOtherKeys detection', () => {
it('should detect modifyOtherKeys support (level 2)', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate modifyOtherKeys level 2 response: \x1b[>4;2m
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
it('should not enable modifyOtherKeys for level 0', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate modifyOtherKeys level 0 response: \x1b[>4;0m
stdin.emit('data', Buffer.from('\x1b[>4;0m'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(enableModifyOtherKeys).not.toHaveBeenCalled();
});
it('should prefer Kitty over modifyOtherKeys', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate both Kitty and modifyOtherKeys responses
stdin.emit('data', Buffer.from('\x1b[?1u'));
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(true);
expect(enableKittyKeyboardProtocol).toHaveBeenCalled();
expect(enableModifyOtherKeys).not.toHaveBeenCalled();
});
it('should enable modifyOtherKeys when Kitty not supported', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate only modifyOtherKeys response (no Kitty)
stdin.emit('data', Buffer.from('\x1b[>4;2m'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(false);
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
it('should handle split modifyOtherKeys response chunks', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Split response: \x1b[>4;2m
stdin.emit('data', Buffer.from('\x1b[>4;'));
stdin.emit('data', Buffer.from('2m'));
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
it('should detect modifyOtherKeys with other capabilities', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
stdin.emit('data', Buffer.from('\x1b]11;rgb:1a1a/1a1a/1a1a\x1b\\')); // background color
stdin.emit('data', Buffer.from('\x1bP>|tmux\x1b\\')); // Terminal name
stdin.emit('data', Buffer.from('\x1b[>4;2m')); // modifyOtherKeys
// Complete detection with DA1
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a');
expect(manager.getTerminalName()).toBe('tmux');
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
it('should not enable modifyOtherKeys without explicit response', async () => {
const manager = TerminalCapabilityManager.getInstance();
const promise = manager.detectCapabilities();
// Simulate only DA1 response (no specific MOK or Kitty response)
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
expect(manager.isKittyProtocolEnabled()).toBe(false);
expect(enableModifyOtherKeys).not.toHaveBeenCalled();
});
it('should wrap queries in hidden/clear sequence', async () => {
const manager = TerminalCapabilityManager.getInstance();
void manager.detectCapabilities();
expect(fs.writeSync).toHaveBeenCalledWith(
expect.anything(),
// eslint-disable-next-line no-control-regex
expect.stringMatching(/^\x1b\[8m.*\x1b\[2K\r\x1b\[0m$/s),
);
});
});
describe('supportsOsc9Notifications', () => {
const manager = TerminalCapabilityManager.getInstance();
it.each([
{
name: 'WezTerm (terminal name)',
terminalName: 'WezTerm',
env: {},
expected: true,
},
{
name: 'iTerm.app (terminal name)',
terminalName: 'iTerm.app',
env: {},
expected: true,
},
{
name: 'ghostty (terminal name)',
terminalName: 'ghostty',
env: {},
expected: true,
},
{
name: 'kitty (terminal name)',
terminalName: 'kitty',
env: {},
expected: true,
},
{
name: 'some-other-term (terminal name)',
terminalName: 'some-other-term',
env: {},
expected: false,
},
{
name: 'iTerm.app (TERM_PROGRAM)',
terminalName: undefined,
env: { TERM_PROGRAM: 'iTerm.app' },
expected: true,
},
{
name: 'vscode (TERM_PROGRAM)',
terminalName: undefined,
env: { TERM_PROGRAM: 'vscode' },
expected: false,
},
{
name: 'xterm-kitty (TERM)',
terminalName: undefined,
env: { TERM: 'xterm-kitty' },
expected: true,
},
{
name: 'xterm-256color (TERM)',
terminalName: undefined,
env: { TERM: 'xterm-256color' },
expected: false,
},
{
name: 'Windows Terminal (WT_SESSION)',
terminalName: 'iTerm.app',
env: { WT_SESSION: 'some-guid' },
expected: false,
},
])(
'should return $expected for $name',
({ terminalName, env, expected }) => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(terminalName);
expect(manager.supportsOsc9Notifications(env)).toBe(expected);
},
);
});
});

View File

@@ -84,6 +84,7 @@ describe('SubAgentInvocation', () => {
params: {},
getDescription: vi.fn(),
toolLocations: vi.fn(),
isSensitive: false,
};
MockSubagentToolWrapper.prototype.build = vi

View File

@@ -11,20 +11,6 @@ export function escapeRegex(text: string): string {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s"]/g, '\\$&');
}
/**
* Escapes a string for use in a regular expression that matches a JSON-stringified value.
*
* This is necessary because some characters (like backslashes and quotes) are
* escaped twice in the final JSON string representation used for policy matching.
*/
export function escapeJsonRegex(text: string): string {
// 1. Get the JSON-escaped version of the string (e.g. C:\foo -> C:\\foo)
// 2. Remove the surrounding quotes
const jsonEscaped = JSON.stringify(text).slice(1, -1);
// 3. Regex-escape the result (e.g. C:\\foo -> C:\\\\foo)
return escapeRegex(jsonEscaped);
}
/**
* Basic validation for regular expressions to prevent common ReDoS patterns.
* This is a heuristic check and not a substitute for a full ReDoS scanner.

View File

@@ -293,6 +293,7 @@ describe('AskUserTool', () => {
getDescription: vi.fn().mockReturnValue(''),
toolLocations: vi.fn().mockReturnValue([]),
shouldConfirmExecute: vi.fn().mockResolvedValue(false),
isSensitive: false,
};
const buildSpy = vi.spyOn(tool, 'build').mockReturnValue(mockInvocation);

View File

@@ -202,6 +202,21 @@ describe('EditTool', () => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('should be marked as sensitive and pass the flag to its invocations', () => {
// Check the tool definition itself
expect(tool.isSensitive).toBe(true);
// Build an invocation and check the instance
const params: EditToolParams = {
file_path: path.join(rootDir, 'test.txt'),
instruction: 'An instruction',
old_string: 'old',
new_string: 'new',
};
const invocation = tool.build(params);
expect(invocation.isSensitive).toBe(true);
});
describe('applyReplacement', () => {
it('should return newString if isNewFile is true', () => {
expect(applyReplacement(null, 'old', 'new', true)).toBe('new');

View File

@@ -16,6 +16,7 @@ class TestToolInvocation implements ToolInvocation<object, ToolResult> {
constructor(
readonly params: object,
private readonly executeFn: () => Promise<ToolResult>,
readonly isSensitive: boolean = false,
) {}
getDescription(): string {

View File

@@ -33,6 +33,11 @@ export interface ToolInvocation<
*/
params: TParams;
/**
* Whether the tool is sensitive and requires specific policy approvals.
*/
isSensitive: boolean;
/**
* Gets a pre-execution description of the tool operation.
*