style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,7 +1,7 @@
import { mock, describe, expect, test, beforeEach, afterEach } from "bun:test";
import { logMock } from "../../../../tests/mocks/log";
import { mock, describe, expect, test, beforeEach, afterEach } from 'bun:test'
import { logMock } from '../../../../tests/mocks/log'
mock.module("src/utils/log.ts", logMock);
mock.module('src/utils/log.ts', logMock)
const {
isExternalPermissionMode,
@@ -14,202 +14,206 @@ const {
getModeColor,
PERMISSION_MODES,
EXTERNAL_PERMISSION_MODES,
} = await import("../PermissionMode");
} = await import('../PermissionMode')
// ─── PERMISSION_MODES / EXTERNAL_PERMISSION_MODES ──────────────────────
describe("PERMISSION_MODES", () => {
test("includes all external modes", () => {
describe('PERMISSION_MODES', () => {
test('includes all external modes', () => {
for (const m of EXTERNAL_PERMISSION_MODES) {
expect(PERMISSION_MODES).toContain(m);
expect(PERMISSION_MODES).toContain(m)
}
});
})
test("includes default, plan, acceptEdits, bypassPermissions, dontAsk", () => {
expect(PERMISSION_MODES).toContain("default");
expect(PERMISSION_MODES).toContain("plan");
expect(PERMISSION_MODES).toContain("acceptEdits");
expect(PERMISSION_MODES).toContain("bypassPermissions");
expect(PERMISSION_MODES).toContain("dontAsk");
});
});
test('includes default, plan, acceptEdits, bypassPermissions, dontAsk', () => {
expect(PERMISSION_MODES).toContain('default')
expect(PERMISSION_MODES).toContain('plan')
expect(PERMISSION_MODES).toContain('acceptEdits')
expect(PERMISSION_MODES).toContain('bypassPermissions')
expect(PERMISSION_MODES).toContain('dontAsk')
})
})
// ─── permissionModeFromString ──────────────────────────────────────────
describe("permissionModeFromString", () => {
test("returns valid mode for known string", () => {
expect(permissionModeFromString("plan")).toBe("plan");
expect(permissionModeFromString("default")).toBe("default");
expect(permissionModeFromString("dontAsk")).toBe("dontAsk");
expect(permissionModeFromString("acceptEdits")).toBe("acceptEdits");
expect(permissionModeFromString("bypassPermissions")).toBe("bypassPermissions");
});
describe('permissionModeFromString', () => {
test('returns valid mode for known string', () => {
expect(permissionModeFromString('plan')).toBe('plan')
expect(permissionModeFromString('default')).toBe('default')
expect(permissionModeFromString('dontAsk')).toBe('dontAsk')
expect(permissionModeFromString('acceptEdits')).toBe('acceptEdits')
expect(permissionModeFromString('bypassPermissions')).toBe(
'bypassPermissions',
)
})
test("returns 'default' for unknown string", () => {
expect(permissionModeFromString("unknown")).toBe("default");
expect(permissionModeFromString("")).toBe("default");
});
expect(permissionModeFromString('unknown')).toBe('default')
expect(permissionModeFromString('')).toBe('default')
})
test("is case sensitive — uppercase returns default", () => {
expect(permissionModeFromString("PLAN")).toBe("default");
expect(permissionModeFromString("Default")).toBe("default");
expect(permissionModeFromString("PLAN")).toBe("default");
});
test('is case sensitive — uppercase returns default', () => {
expect(permissionModeFromString('PLAN')).toBe('default')
expect(permissionModeFromString('Default')).toBe('default')
expect(permissionModeFromString('PLAN')).toBe('default')
})
test("returns mode for all known external modes", () => {
test('returns mode for all known external modes', () => {
for (const mode of EXTERNAL_PERMISSION_MODES) {
expect(permissionModeFromString(mode)).toBe(mode);
expect(permissionModeFromString(mode)).toBe(mode)
}
});
});
})
})
// ─── permissionModeTitle ───────────────────────────────────────────────
describe("permissionModeTitle", () => {
test("returns title for known modes", () => {
expect(permissionModeTitle("default")).toBe("Default");
expect(permissionModeTitle("plan")).toBe("Plan Mode");
expect(permissionModeTitle("acceptEdits")).toBe("Accept edits");
expect(permissionModeTitle("bypassPermissions")).toBe("Bypass");
expect(permissionModeTitle("dontAsk")).toBe("Don't Ask");
});
describe('permissionModeTitle', () => {
test('returns title for known modes', () => {
expect(permissionModeTitle('default')).toBe('Default')
expect(permissionModeTitle('plan')).toBe('Plan Mode')
expect(permissionModeTitle('acceptEdits')).toBe('Accept edits')
expect(permissionModeTitle('bypassPermissions')).toBe('Bypass')
expect(permissionModeTitle('dontAsk')).toBe("Don't Ask")
})
test("falls back to Default for unknown mode", () => {
expect(permissionModeTitle("nonexistent" as any)).toBe("Default");
});
});
test('falls back to Default for unknown mode', () => {
expect(permissionModeTitle('nonexistent' as any)).toBe('Default')
})
})
// ─── permissionModeShortTitle ──────────────────────────────────────────
describe("permissionModeShortTitle", () => {
test("returns short title for known modes", () => {
expect(permissionModeShortTitle("default")).toBe("Default");
expect(permissionModeShortTitle("plan")).toBe("Plan");
expect(permissionModeShortTitle("bypassPermissions")).toBe("Bypass");
expect(permissionModeShortTitle("dontAsk")).toBe("DontAsk");
expect(permissionModeShortTitle("acceptEdits")).toBe("Accept");
});
});
describe('permissionModeShortTitle', () => {
test('returns short title for known modes', () => {
expect(permissionModeShortTitle('default')).toBe('Default')
expect(permissionModeShortTitle('plan')).toBe('Plan')
expect(permissionModeShortTitle('bypassPermissions')).toBe('Bypass')
expect(permissionModeShortTitle('dontAsk')).toBe('DontAsk')
expect(permissionModeShortTitle('acceptEdits')).toBe('Accept')
})
})
// ─── permissionModeSymbol ──────────────────────────────────────────────
describe("permissionModeSymbol", () => {
test("returns empty string for default", () => {
expect(permissionModeSymbol("default")).toBe("");
});
describe('permissionModeSymbol', () => {
test('returns empty string for default', () => {
expect(permissionModeSymbol('default')).toBe('')
})
test("returns non-empty for non-default modes", () => {
expect(permissionModeSymbol("plan").length).toBeGreaterThan(0);
expect(permissionModeSymbol("acceptEdits").length).toBeGreaterThan(0);
});
});
test('returns non-empty for non-default modes', () => {
expect(permissionModeSymbol('plan').length).toBeGreaterThan(0)
expect(permissionModeSymbol('acceptEdits').length).toBeGreaterThan(0)
})
})
// ─── getModeColor ──────────────────────────────────────────────────────
describe("getModeColor", () => {
describe('getModeColor', () => {
test("returns 'text' for default", () => {
expect(getModeColor("default")).toBe("text");
});
expect(getModeColor('default')).toBe('text')
})
test("returns 'planMode' for plan", () => {
expect(getModeColor("plan")).toBe("planMode");
});
expect(getModeColor('plan')).toBe('planMode')
})
test("returns 'error' for bypassPermissions", () => {
expect(getModeColor("bypassPermissions")).toBe("error");
});
expect(getModeColor('bypassPermissions')).toBe('error')
})
test("returns 'error' for dontAsk", () => {
expect(getModeColor("dontAsk")).toBe("error");
});
expect(getModeColor('dontAsk')).toBe('error')
})
test("returns 'autoAccept' for acceptEdits", () => {
expect(getModeColor("acceptEdits")).toBe("autoAccept");
});
});
expect(getModeColor('acceptEdits')).toBe('autoAccept')
})
})
// ─── isDefaultMode ─────────────────────────────────────────────────────
describe("isDefaultMode", () => {
describe('isDefaultMode', () => {
test("returns true for 'default'", () => {
expect(isDefaultMode("default")).toBe(true);
});
expect(isDefaultMode('default')).toBe(true)
})
test("returns true for undefined", () => {
expect(isDefaultMode(undefined)).toBe(true);
});
test('returns true for undefined', () => {
expect(isDefaultMode(undefined)).toBe(true)
})
test("returns false for other modes", () => {
expect(isDefaultMode("plan")).toBe(false);
expect(isDefaultMode("dontAsk")).toBe(false);
});
});
test('returns false for other modes', () => {
expect(isDefaultMode('plan')).toBe(false)
expect(isDefaultMode('dontAsk')).toBe(false)
})
})
// ─── toExternalPermissionMode ──────────────────────────────────────────
describe("toExternalPermissionMode", () => {
test("maps default to default", () => {
expect(toExternalPermissionMode("default")).toBe("default");
});
describe('toExternalPermissionMode', () => {
test('maps default to default', () => {
expect(toExternalPermissionMode('default')).toBe('default')
})
test("maps plan to plan", () => {
expect(toExternalPermissionMode("plan")).toBe("plan");
});
test('maps plan to plan', () => {
expect(toExternalPermissionMode('plan')).toBe('plan')
})
test("maps dontAsk to dontAsk", () => {
expect(toExternalPermissionMode("dontAsk")).toBe("dontAsk");
});
test('maps dontAsk to dontAsk', () => {
expect(toExternalPermissionMode('dontAsk')).toBe('dontAsk')
})
test("maps acceptEdits to acceptEdits", () => {
expect(toExternalPermissionMode("acceptEdits")).toBe("acceptEdits");
});
test('maps acceptEdits to acceptEdits', () => {
expect(toExternalPermissionMode('acceptEdits')).toBe('acceptEdits')
})
test("maps bypassPermissions to bypassPermissions", () => {
expect(toExternalPermissionMode("bypassPermissions")).toBe("bypassPermissions");
});
});
test('maps bypassPermissions to bypassPermissions', () => {
expect(toExternalPermissionMode('bypassPermissions')).toBe(
'bypassPermissions',
)
})
})
// ─── isExternalPermissionMode ──────────────────────────────────────────
describe("isExternalPermissionMode", () => {
test("returns true for external modes (non-ant)", () => {
describe('isExternalPermissionMode', () => {
test('returns true for external modes (non-ant)', () => {
// USER_TYPE is not 'ant' in tests, so always true
expect(isExternalPermissionMode("default")).toBe(true);
expect(isExternalPermissionMode("plan")).toBe(true);
});
expect(isExternalPermissionMode('default')).toBe(true)
expect(isExternalPermissionMode('plan')).toBe(true)
})
describe("when USER_TYPE is 'ant'", () => {
const savedUserType = process.env.USER_TYPE;
const savedUserType = process.env.USER_TYPE
beforeEach(() => {
process.env.USER_TYPE = "ant";
});
process.env.USER_TYPE = 'ant'
})
afterEach(() => {
if (savedUserType !== undefined) {
process.env.USER_TYPE = savedUserType;
process.env.USER_TYPE = savedUserType
} else {
delete process.env.USER_TYPE;
delete process.env.USER_TYPE
}
});
})
test("returns false for 'auto' (ant-only mode)", () => {
expect(isExternalPermissionMode("auto")).toBe(false);
});
expect(isExternalPermissionMode('auto')).toBe(false)
})
test("returns false for 'bubble' (ant-only mode)", () => {
expect(isExternalPermissionMode("bubble")).toBe(false);
});
expect(isExternalPermissionMode('bubble')).toBe(false)
})
test("returns true for standard external modes", () => {
expect(isExternalPermissionMode("default")).toBe(true);
expect(isExternalPermissionMode("plan")).toBe(true);
expect(isExternalPermissionMode("dontAsk")).toBe(true);
});
test('returns true for standard external modes', () => {
expect(isExternalPermissionMode('default')).toBe(true)
expect(isExternalPermissionMode('plan')).toBe(true)
expect(isExternalPermissionMode('dontAsk')).toBe(true)
})
test("returns true for acceptEdits and bypassPermissions", () => {
expect(isExternalPermissionMode("acceptEdits")).toBe(true);
expect(isExternalPermissionMode("bypassPermissions")).toBe(true);
});
});
});
test('returns true for acceptEdits and bypassPermissions', () => {
expect(isExternalPermissionMode('acceptEdits')).toBe(true)
expect(isExternalPermissionMode('bypassPermissions')).toBe(true)
})
})
})

View File

@@ -1,93 +1,93 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
CROSS_PLATFORM_CODE_EXEC,
DANGEROUS_BASH_PATTERNS,
} from "../dangerousPatterns";
} from '../dangerousPatterns'
describe("CROSS_PLATFORM_CODE_EXEC", () => {
test("is a non-empty readonly array of strings", () => {
expect(CROSS_PLATFORM_CODE_EXEC.length).toBeGreaterThan(0);
describe('CROSS_PLATFORM_CODE_EXEC', () => {
test('is a non-empty readonly array of strings', () => {
expect(CROSS_PLATFORM_CODE_EXEC.length).toBeGreaterThan(0)
for (const p of CROSS_PLATFORM_CODE_EXEC) {
expect(typeof p).toBe("string");
expect(typeof p).toBe('string')
}
});
})
test("includes core interpreters", () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain("python");
expect(CROSS_PLATFORM_CODE_EXEC).toContain("node");
expect(CROSS_PLATFORM_CODE_EXEC).toContain("ruby");
expect(CROSS_PLATFORM_CODE_EXEC).toContain("perl");
});
test('includes core interpreters', () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain('python')
expect(CROSS_PLATFORM_CODE_EXEC).toContain('node')
expect(CROSS_PLATFORM_CODE_EXEC).toContain('ruby')
expect(CROSS_PLATFORM_CODE_EXEC).toContain('perl')
})
test("includes package runners", () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain("npx");
expect(CROSS_PLATFORM_CODE_EXEC).toContain("bunx");
});
test('includes package runners', () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain('npx')
expect(CROSS_PLATFORM_CODE_EXEC).toContain('bunx')
})
test("includes shells", () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain("bash");
expect(CROSS_PLATFORM_CODE_EXEC).toContain("sh");
});
test('includes shells', () => {
expect(CROSS_PLATFORM_CODE_EXEC).toContain('bash')
expect(CROSS_PLATFORM_CODE_EXEC).toContain('sh')
})
test("has no duplicate entries", () => {
test('has no duplicate entries', () => {
expect(new Set(CROSS_PLATFORM_CODE_EXEC).size).toBe(
CROSS_PLATFORM_CODE_EXEC.length
);
});
CROSS_PLATFORM_CODE_EXEC.length,
)
})
test("contains expected interpreters", () => {
test('contains expected interpreters', () => {
const expected = [
"node",
"python",
"python3",
"ruby",
"perl",
"php",
"lua",
"deno",
"npx",
"bunx",
"tsx",
];
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
'node',
'python',
'python3',
'ruby',
'perl',
'php',
'lua',
'deno',
'npx',
'bunx',
'tsx',
]
const set = new Set(CROSS_PLATFORM_CODE_EXEC)
for (const entry of expected) {
expect(set.has(entry as any)).toBe(true);
expect(set.has(entry as any)).toBe(true)
}
});
});
})
})
describe("DANGEROUS_BASH_PATTERNS", () => {
test("includes all cross-platform patterns", () => {
describe('DANGEROUS_BASH_PATTERNS', () => {
test('includes all cross-platform patterns', () => {
for (const p of CROSS_PLATFORM_CODE_EXEC) {
expect(DANGEROUS_BASH_PATTERNS).toContain(p);
expect(DANGEROUS_BASH_PATTERNS).toContain(p)
}
});
})
test("includes unix-specific patterns", () => {
expect(DANGEROUS_BASH_PATTERNS).toContain("zsh");
expect(DANGEROUS_BASH_PATTERNS).toContain("fish");
expect(DANGEROUS_BASH_PATTERNS).toContain("eval");
expect(DANGEROUS_BASH_PATTERNS).toContain("exec");
expect(DANGEROUS_BASH_PATTERNS).toContain("sudo");
expect(DANGEROUS_BASH_PATTERNS).toContain("xargs");
expect(DANGEROUS_BASH_PATTERNS).toContain("env");
});
test('includes unix-specific patterns', () => {
expect(DANGEROUS_BASH_PATTERNS).toContain('zsh')
expect(DANGEROUS_BASH_PATTERNS).toContain('fish')
expect(DANGEROUS_BASH_PATTERNS).toContain('eval')
expect(DANGEROUS_BASH_PATTERNS).toContain('exec')
expect(DANGEROUS_BASH_PATTERNS).toContain('sudo')
expect(DANGEROUS_BASH_PATTERNS).toContain('xargs')
expect(DANGEROUS_BASH_PATTERNS).toContain('env')
})
test("all elements are strings", () => {
test('all elements are strings', () => {
for (const p of DANGEROUS_BASH_PATTERNS) {
expect(typeof p).toBe("string");
expect(typeof p).toBe('string')
}
});
})
test("has no duplicate entries", () => {
test('has no duplicate entries', () => {
expect(new Set(DANGEROUS_BASH_PATTERNS).size).toBe(
DANGEROUS_BASH_PATTERNS.length
);
});
DANGEROUS_BASH_PATTERNS.length,
)
})
test("empty string does not match any pattern", () => {
test('empty string does not match any pattern', () => {
for (const pattern of DANGEROUS_BASH_PATTERNS) {
expect("".startsWith(pattern)).toBe(false);
expect(''.startsWith(pattern)).toBe(false)
}
});
});
})
})

View File

@@ -52,11 +52,15 @@ describe('getNextPermissionMode', () => {
})
test('auto → bypassPermissions (when bypass available)', () => {
expect(getNextPermissionMode(makeContext('auto'))).toBe('bypassPermissions')
expect(getNextPermissionMode(makeContext('auto'))).toBe(
'bypassPermissions',
)
})
test('bypassPermissions → default', () => {
expect(getNextPermissionMode(makeContext('bypassPermissions'))).toBe('default')
expect(getNextPermissionMode(makeContext('bypassPermissions'))).toBe(
'default',
)
})
test('full cycle completes back to default', () => {

View File

@@ -1,152 +1,152 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
escapeRuleContent,
unescapeRuleContent,
permissionRuleValueFromString,
permissionRuleValueToString,
normalizeLegacyToolName,
} from "../permissionRuleParser";
} from '../permissionRuleParser'
describe("escapeRuleContent", () => {
test("escapes backslashes first", () => {
expect(escapeRuleContent("a\\b")).toBe("a\\\\b");
});
describe('escapeRuleContent', () => {
test('escapes backslashes first', () => {
expect(escapeRuleContent('a\\b')).toBe('a\\\\b')
})
test("escapes opening parentheses", () => {
expect(escapeRuleContent("fn(x)")).toBe("fn\\(x\\)");
});
test('escapes opening parentheses', () => {
expect(escapeRuleContent('fn(x)')).toBe('fn\\(x\\)')
})
test("escapes backslash before parens correctly", () => {
test('escapes backslash before parens correctly', () => {
expect(escapeRuleContent('echo "test\\nvalue"')).toBe(
'echo "test\\\\nvalue"'
);
});
'echo "test\\\\nvalue"',
)
})
test("returns unchanged string with no special chars", () => {
expect(escapeRuleContent("npm install")).toBe("npm install");
});
test('returns unchanged string with no special chars', () => {
expect(escapeRuleContent('npm install')).toBe('npm install')
})
test("handles empty string", () => {
expect(escapeRuleContent("")).toBe("");
});
});
test('handles empty string', () => {
expect(escapeRuleContent('')).toBe('')
})
})
describe("unescapeRuleContent", () => {
test("unescapes parentheses", () => {
expect(unescapeRuleContent("fn\\(x\\)")).toBe("fn(x)");
});
describe('unescapeRuleContent', () => {
test('unescapes parentheses', () => {
expect(unescapeRuleContent('fn\\(x\\)')).toBe('fn(x)')
})
test("unescapes backslashes", () => {
expect(unescapeRuleContent("a\\\\b")).toBe("a\\b");
});
test('unescapes backslashes', () => {
expect(unescapeRuleContent('a\\\\b')).toBe('a\\b')
})
test("roundtrips with escapeRuleContent", () => {
const original = 'python -c "print(1)"';
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original);
});
test('roundtrips with escapeRuleContent', () => {
const original = 'python -c "print(1)"'
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original)
})
test("handles content with backslash-paren combo", () => {
const original = 'echo "test\\nvalue"';
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original);
});
test('handles content with backslash-paren combo', () => {
const original = 'echo "test\\nvalue"'
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original)
})
test("returns unchanged string with no escapes", () => {
expect(unescapeRuleContent("npm install")).toBe("npm install");
});
});
test('returns unchanged string with no escapes', () => {
expect(unescapeRuleContent('npm install')).toBe('npm install')
})
})
describe("permissionRuleValueFromString", () => {
test("parses tool name only", () => {
expect(permissionRuleValueFromString("Bash")).toEqual({
toolName: "Bash",
});
});
describe('permissionRuleValueFromString', () => {
test('parses tool name only', () => {
expect(permissionRuleValueFromString('Bash')).toEqual({
toolName: 'Bash',
})
})
test("parses tool name with content", () => {
expect(permissionRuleValueFromString("Bash(npm install)")).toEqual({
toolName: "Bash",
ruleContent: "npm install",
});
});
test('parses tool name with content', () => {
expect(permissionRuleValueFromString('Bash(npm install)')).toEqual({
toolName: 'Bash',
ruleContent: 'npm install',
})
})
test("handles escaped parens in content", () => {
test('handles escaped parens in content', () => {
const result = permissionRuleValueFromString(
'Bash(python -c "print\\(1\\)")'
);
expect(result.toolName).toBe("Bash");
expect(result.ruleContent).toBe('python -c "print(1)"');
});
'Bash(python -c "print\\(1\\)")',
)
expect(result.toolName).toBe('Bash')
expect(result.ruleContent).toBe('python -c "print(1)"')
})
test("treats empty content as tool-wide rule", () => {
expect(permissionRuleValueFromString("Bash()")).toEqual({
toolName: "Bash",
});
});
test('treats empty content as tool-wide rule', () => {
expect(permissionRuleValueFromString('Bash()')).toEqual({
toolName: 'Bash',
})
})
test("treats wildcard content as tool-wide rule", () => {
expect(permissionRuleValueFromString("Bash(*)")).toEqual({
toolName: "Bash",
});
});
test('treats wildcard content as tool-wide rule', () => {
expect(permissionRuleValueFromString('Bash(*)')).toEqual({
toolName: 'Bash',
})
})
test("normalizes legacy tool names", () => {
const result = permissionRuleValueFromString("Task");
expect(result.toolName).toBe("Agent");
});
test('normalizes legacy tool names', () => {
const result = permissionRuleValueFromString('Task')
expect(result.toolName).toBe('Agent')
})
test("handles MCP-style tool names", () => {
expect(permissionRuleValueFromString("mcp__server__tool")).toEqual({
toolName: "mcp__server__tool",
});
});
});
test('handles MCP-style tool names', () => {
expect(permissionRuleValueFromString('mcp__server__tool')).toEqual({
toolName: 'mcp__server__tool',
})
})
})
describe("permissionRuleValueToString", () => {
test("formats tool name only", () => {
expect(permissionRuleValueToString({ toolName: "Bash" })).toBe("Bash");
});
describe('permissionRuleValueToString', () => {
test('formats tool name only', () => {
expect(permissionRuleValueToString({ toolName: 'Bash' })).toBe('Bash')
})
test("formats tool name with content", () => {
test('formats tool name with content', () => {
expect(
permissionRuleValueToString({
toolName: "Bash",
ruleContent: "npm install",
})
).toBe("Bash(npm install)");
});
toolName: 'Bash',
ruleContent: 'npm install',
}),
).toBe('Bash(npm install)')
})
test("escapes parens in content", () => {
test('escapes parens in content', () => {
expect(
permissionRuleValueToString({
toolName: "Bash",
toolName: 'Bash',
ruleContent: 'python -c "print(1)"',
})
).toBe('Bash(python -c "print\\(1\\)")');
});
}),
).toBe('Bash(python -c "print\\(1\\)")')
})
test("roundtrips with permissionRuleValueFromString", () => {
const original = { toolName: "Bash", ruleContent: "npm install" };
const str = permissionRuleValueToString(original);
const parsed = permissionRuleValueFromString(str);
expect(parsed).toEqual(original);
});
});
test('roundtrips with permissionRuleValueFromString', () => {
const original = { toolName: 'Bash', ruleContent: 'npm install' }
const str = permissionRuleValueToString(original)
const parsed = permissionRuleValueFromString(str)
expect(parsed).toEqual(original)
})
})
describe("normalizeLegacyToolName", () => {
test("maps Task to Agent", () => {
expect(normalizeLegacyToolName("Task")).toBe("Agent");
});
describe('normalizeLegacyToolName', () => {
test('maps Task to Agent', () => {
expect(normalizeLegacyToolName('Task')).toBe('Agent')
})
test("maps KillShell to TaskStop", () => {
expect(normalizeLegacyToolName("KillShell")).toBe("TaskStop");
});
test('maps KillShell to TaskStop', () => {
expect(normalizeLegacyToolName('KillShell')).toBe('TaskStop')
})
test("returns unknown name as-is", () => {
expect(normalizeLegacyToolName("UnknownTool")).toBe("UnknownTool");
});
test('returns unknown name as-is', () => {
expect(normalizeLegacyToolName('UnknownTool')).toBe('UnknownTool')
})
test("preserves current canonical names", () => {
expect(normalizeLegacyToolName("Bash")).toBe("Bash");
expect(normalizeLegacyToolName("Agent")).toBe("Agent");
});
});
test('preserves current canonical names', () => {
expect(normalizeLegacyToolName('Bash')).toBe('Bash')
expect(normalizeLegacyToolName('Agent')).toBe('Agent')
})
})

View File

@@ -40,7 +40,9 @@ describe('permission gate invariants (after opening auto/bypass)', () => {
describe('bypass mode always reachable in cycle', () => {
test('auto → bypassPermissions when isBypassPermissionsModeAvailable is true', () => {
const ctx = makeContext('auto', { isBypassPermissionsModeAvailable: true })
const ctx = makeContext('auto', {
isBypassPermissionsModeAvailable: true,
})
expect(getNextPermissionMode(ctx)).toBe('bypassPermissions')
})
@@ -61,7 +63,9 @@ describe('permission gate invariants (after opening auto/bypass)', () => {
})
test('plan → auto even when isBypassPermissionsModeAvailable is false', () => {
const ctx = makeContext('plan', { isBypassPermissionsModeAvailable: false })
const ctx = makeContext('plan', {
isBypassPermissionsModeAvailable: false,
})
expect(getNextPermissionMode(ctx)).toBe('auto')
})
@@ -106,7 +110,12 @@ describe('permission gate invariants (after opening auto/bypass)', () => {
steps.push(mode)
}
expect(steps).toEqual(['acceptEdits', 'plan', 'auto', 'bypassPermissions'])
expect(steps).toEqual([
'acceptEdits',
'plan',
'auto',
'bypassPermissions',
])
})
})

View File

@@ -22,7 +22,10 @@ function makeContext(opts: { denyRules?: string[]; askRules?: string[] }) {
return { ...ctx, alwaysDenyRules: deny, alwaysAskRules: ask } as any
}
function makeTool(name: string, mcpInfo?: { serverName: string; toolName: string }) {
function makeTool(
name: string,
mcpInfo?: { serverName: string; toolName: string },
) {
return { name, mcpInfo }
}
@@ -88,7 +91,9 @@ describe('Langfuse trace propagation', () => {
messages: [],
abortController: new AbortController(),
readFileState: createFileStateCacheWithSizeLimit(1),
getAppState: () => ({ toolPermissionContext: getEmptyToolPermissionContext() }),
getAppState: () => ({
toolPermissionContext: getEmptyToolPermissionContext(),
}),
setAppState: () => {},
updateFileHistoryState: () => {},
updateAttributionState: () => {},
@@ -115,7 +120,9 @@ describe('filterDeniedAgents', () => {
expect(result[0]!.agentType).toBe('Research')
})
test('returns empty array when all agents denied', () => {
const ctx = makeContext({ denyRules: ['Agent(Explore)', 'Agent(Research)'] })
const ctx = makeContext({
denyRules: ['Agent(Explore)', 'Agent(Research)'],
})
const agents = [{ agentType: 'Explore' }, { agentType: 'Research' }]
expect(filterDeniedAgents(agents, ctx, 'Agent')).toEqual([])
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
permissionRuleExtractPrefix,
hasWildcards,
@@ -6,140 +6,142 @@ import {
parsePermissionRule,
suggestionForExactCommand,
suggestionForPrefix,
} from "../shellRuleMatching";
} from '../shellRuleMatching'
// ─── permissionRuleExtractPrefix ────────────────────────────────────────
describe("permissionRuleExtractPrefix", () => {
test("extracts prefix from legacy :* syntax", () => {
expect(permissionRuleExtractPrefix("npm:*")).toBe("npm");
});
describe('permissionRuleExtractPrefix', () => {
test('extracts prefix from legacy :* syntax', () => {
expect(permissionRuleExtractPrefix('npm:*')).toBe('npm')
})
test("extracts multi-word prefix", () => {
expect(permissionRuleExtractPrefix("git commit:*")).toBe("git commit");
});
test('extracts multi-word prefix', () => {
expect(permissionRuleExtractPrefix('git commit:*')).toBe('git commit')
})
test("returns null for non-prefix rule", () => {
expect(permissionRuleExtractPrefix("npm install")).toBeNull();
});
test('returns null for non-prefix rule', () => {
expect(permissionRuleExtractPrefix('npm install')).toBeNull()
})
test("returns null for empty string", () => {
expect(permissionRuleExtractPrefix("")).toBeNull();
});
test('returns null for empty string', () => {
expect(permissionRuleExtractPrefix('')).toBeNull()
})
test("returns null for wildcard without colon", () => {
expect(permissionRuleExtractPrefix("npm *")).toBeNull();
});
});
test('returns null for wildcard without colon', () => {
expect(permissionRuleExtractPrefix('npm *')).toBeNull()
})
})
// ─── hasWildcards ───────────────────────────────────────────────────────
describe("hasWildcards", () => {
test("returns true for unescaped wildcard", () => {
expect(hasWildcards("git *")).toBe(true);
});
describe('hasWildcards', () => {
test('returns true for unescaped wildcard', () => {
expect(hasWildcards('git *')).toBe(true)
})
test("returns false for legacy :* syntax", () => {
expect(hasWildcards("npm:*")).toBe(false);
});
test('returns false for legacy :* syntax', () => {
expect(hasWildcards('npm:*')).toBe(false)
})
test("returns false for escaped wildcard", () => {
expect(hasWildcards("git \\*")).toBe(false);
});
test('returns false for escaped wildcard', () => {
expect(hasWildcards('git \\*')).toBe(false)
})
test("returns true for * with even backslashes", () => {
expect(hasWildcards("git \\\\*")).toBe(true);
});
test('returns true for * with even backslashes', () => {
expect(hasWildcards('git \\\\*')).toBe(true)
})
test("returns false for no wildcards", () => {
expect(hasWildcards("npm install")).toBe(false);
});
test('returns false for no wildcards', () => {
expect(hasWildcards('npm install')).toBe(false)
})
test("returns false for empty string", () => {
expect(hasWildcards("")).toBe(false);
});
});
test('returns false for empty string', () => {
expect(hasWildcards('')).toBe(false)
})
})
// ─── matchWildcardPattern ───────────────────────────────────────────────
describe("matchWildcardPattern", () => {
test("matches simple wildcard", () => {
expect(matchWildcardPattern("git *", "git add")).toBe(true);
});
describe('matchWildcardPattern', () => {
test('matches simple wildcard', () => {
expect(matchWildcardPattern('git *', 'git add')).toBe(true)
})
test("matches bare command when pattern ends with space-wildcard", () => {
expect(matchWildcardPattern("git *", "git")).toBe(true);
});
test('matches bare command when pattern ends with space-wildcard', () => {
expect(matchWildcardPattern('git *', 'git')).toBe(true)
})
test("rejects non-matching command", () => {
expect(matchWildcardPattern("git *", "npm install")).toBe(false);
});
test('rejects non-matching command', () => {
expect(matchWildcardPattern('git *', 'npm install')).toBe(false)
})
test("matches middle wildcard", () => {
expect(matchWildcardPattern("git * --verbose", "git add --verbose")).toBe(true);
});
test('matches middle wildcard', () => {
expect(matchWildcardPattern('git * --verbose', 'git add --verbose')).toBe(
true,
)
})
test("handles escaped asterisk as literal", () => {
expect(matchWildcardPattern("echo \\*", "echo *")).toBe(true);
expect(matchWildcardPattern("echo \\*", "echo hello")).toBe(false);
});
test('handles escaped asterisk as literal', () => {
expect(matchWildcardPattern('echo \\*', 'echo *')).toBe(true)
expect(matchWildcardPattern('echo \\*', 'echo hello')).toBe(false)
})
test("case-insensitive matching", () => {
expect(matchWildcardPattern("Git *", "git add", true)).toBe(true);
});
test('case-insensitive matching', () => {
expect(matchWildcardPattern('Git *', 'git add', true)).toBe(true)
})
test("exact match without wildcards", () => {
expect(matchWildcardPattern("npm install", "npm install")).toBe(true);
expect(matchWildcardPattern("npm install", "npm update")).toBe(false);
});
test('exact match without wildcards', () => {
expect(matchWildcardPattern('npm install', 'npm install')).toBe(true)
expect(matchWildcardPattern('npm install', 'npm update')).toBe(false)
})
test("handles regex special characters in pattern", () => {
expect(matchWildcardPattern("echo (hello)", "echo (hello)")).toBe(true);
});
});
test('handles regex special characters in pattern', () => {
expect(matchWildcardPattern('echo (hello)', 'echo (hello)')).toBe(true)
})
})
// ─── parsePermissionRule ────────────────────────────────────────────────
describe("parsePermissionRule", () => {
test("parses exact command", () => {
const result = parsePermissionRule("npm install");
expect(result).toEqual({ type: "exact", command: "npm install" });
});
describe('parsePermissionRule', () => {
test('parses exact command', () => {
const result = parsePermissionRule('npm install')
expect(result).toEqual({ type: 'exact', command: 'npm install' })
})
test("parses legacy prefix syntax", () => {
const result = parsePermissionRule("npm:*");
expect(result).toEqual({ type: "prefix", prefix: "npm" });
});
test('parses legacy prefix syntax', () => {
const result = parsePermissionRule('npm:*')
expect(result).toEqual({ type: 'prefix', prefix: 'npm' })
})
test("parses wildcard pattern", () => {
const result = parsePermissionRule("git *");
expect(result).toEqual({ type: "wildcard", pattern: "git *" });
});
test('parses wildcard pattern', () => {
const result = parsePermissionRule('git *')
expect(result).toEqual({ type: 'wildcard', pattern: 'git *' })
})
test("escaped wildcard is treated as exact", () => {
const result = parsePermissionRule("echo \\*");
expect(result.type).toBe("exact");
});
});
test('escaped wildcard is treated as exact', () => {
const result = parsePermissionRule('echo \\*')
expect(result.type).toBe('exact')
})
})
// ─── suggestionForExactCommand ──────────────────────────────────────────
describe("suggestionForExactCommand", () => {
test("creates addRules suggestion", () => {
const result = suggestionForExactCommand("Bash", "npm install");
expect(result).toHaveLength(1);
expect(result[0]!.type).toBe("addRules");
expect((result[0] as any).rules[0]!.toolName).toBe("Bash");
expect((result[0] as any).rules[0]!.ruleContent).toBe("npm install");
expect((result[0] as any).behavior).toBe("allow");
});
});
describe('suggestionForExactCommand', () => {
test('creates addRules suggestion', () => {
const result = suggestionForExactCommand('Bash', 'npm install')
expect(result).toHaveLength(1)
expect(result[0]!.type).toBe('addRules')
expect((result[0] as any).rules[0]!.toolName).toBe('Bash')
expect((result[0] as any).rules[0]!.ruleContent).toBe('npm install')
expect((result[0] as any).behavior).toBe('allow')
})
})
// ─── suggestionForPrefix ────────────────────────────────────────────────
describe("suggestionForPrefix", () => {
test("creates prefix suggestion with :*", () => {
const result = suggestionForPrefix("Bash", "npm");
expect((result[0] as any).rules[0]!.ruleContent).toBe("npm:*");
});
});
describe('suggestionForPrefix', () => {
test('creates prefix suggestion with :*', () => {
const result = suggestionForPrefix('Bash', 'npm')
expect((result[0] as any).rules[0]!.ruleContent).toBe('npm:*')
})
})

View File

@@ -4,18 +4,24 @@ import { useNotifications } from 'src/context/notifications.js'
import { toError } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useAppState, useAppStateStore, useSetAppState } from '../../state/AppState.js'
import type { ToolPermissionContext } from '../../Tool.js'
import {
verifyAutoModeGateAccess,
} from './permissionSetup.js'
useAppState,
useAppStateStore,
useSetAppState,
} from '../../state/AppState.js'
import type { ToolPermissionContext } from '../../Tool.js'
import { verifyAutoModeGateAccess } from './permissionSetup.js'
/**
* No-op — bypass permissions is always available.
*/
export async function checkAndDisableBypassPermissionsIfNeeded(
_toolPermissionContext: ToolPermissionContext,
_setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
_setAppState: (
f: (
prev: import('../../state/AppState.js').AppState,
) => import('../../state/AppState.js').AppState,
) => void,
): Promise<void> {
// Bypass permissions is always available — no gate check needed
}
@@ -38,7 +44,11 @@ let autoModeCheckRan = false
export async function checkAndDisableAutoModeIfNeeded(
toolPermissionContext: ToolPermissionContext,
setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
setAppState: (
f: (
prev: import('../../state/AppState.js').AppState,
) => import('../../state/AppState.js').AppState,
) => void,
fastMode?: boolean,
): Promise<void> {
if (feature('TRANSCRIPT_CLASSIFIER')) {
@@ -106,7 +116,9 @@ export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
setAppState,
fastMode,
).catch(error => {
logError(new Error('Auto mode gate check failed', { cause: toError(error) }))
logError(
new Error('Auto mode gate check failed', { cause: toError(error) }),
)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainLoopModel, mainLoopModelForSession, fastMode])

View File

@@ -1325,7 +1325,11 @@ export function checkWritePermissionForTool<Input extends AnyObject>(
},
]
: generateSuggestions(path, 'write', toolPermissionContext, pathsToCheck)
const failedCheck = safetyCheck as { safe: false; message: string; classifierApprovable: boolean }
const failedCheck = safetyCheck as {
safe: false
message: string
classifierApprovable: boolean
}
return {
behavior: 'ask',
message: failedCheck.message,

View File

@@ -112,8 +112,12 @@ export function isPathInSandboxWriteAllowlist(resolvedPath: string): boolean {
// their resolution to avoid N × config.length redundant syscalls per
// command with N write targets (matching getResolvedWorkingDirPaths).
const pathsToCheck = getPathsForPermissionCheck(resolvedPath)
const resolvedAllow = allowOnly.flatMap(getResolvedSandboxConfigPath) as string[]
const resolvedDeny = denyWithinAllow.flatMap(getResolvedSandboxConfigPath) as string[]
const resolvedAllow = allowOnly.flatMap(
getResolvedSandboxConfigPath,
) as string[]
const resolvedDeny = denyWithinAllow.flatMap(
getResolvedSandboxConfigPath,
) as string[]
return pathsToCheck.every(p => {
for (const denyPath of resolvedDeny) {
if (pathInWorkingPath(p, denyPath)) return false
@@ -184,7 +188,11 @@ export function isPathAllowed(
precomputedPathsToCheck,
)
if (!safetyCheck.safe) {
const failedCheck = safetyCheck as { safe: false; message: string; classifierApprovable: boolean }
const failedCheck = safetyCheck as {
safe: false
message: string
classifierApprovable: boolean
}
return {
allowed: false,
decisionReason: {

View File

@@ -114,7 +114,9 @@ function extractConversationContext(
for (const msg of assistantMessages.reverse()) {
// Extract text content from assistant message
const textBlocks = (Array.isArray(msg.message.content) ? msg.message.content : [])
const textBlocks = (
Array.isArray(msg.message.content) ? msg.message.content : []
)
.filter(c => c.type === 'text')
.map(c => ('text' in c ? c.text : ''))
.join(' ')

View File

@@ -479,7 +479,6 @@ export const hasPermissionsToUseTool: CanUseToolFn = async (
): Promise<PermissionDecision> => {
const result = await hasPermissionsToUseToolInner(tool, input, context)
// Reset consecutive denials on any allowed tool use in auto mode.
// This ensures that a successful tool use (even one auto-allowed by rules)
// breaks the consecutive denial streak.

View File

@@ -304,7 +304,10 @@ export type TranscriptEntry = {
export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
const transcript: TranscriptEntry[] = []
for (const msg of messages) {
if (msg.type === 'attachment' && msg.attachment!.type === 'queued_command') {
if (
msg.type === 'attachment' &&
msg.attachment!.type === 'queued_command'
) {
const prompt = msg.attachment!.prompt
let text: string | null = null
if (typeof prompt === 'string') {
@@ -342,7 +345,7 @@ export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
}
} else if (msg.type === 'assistant') {
const blocks: TranscriptBlock[] = []
for (const block of (msg.message!.content ?? [])) {
for (const block of msg.message!.content ?? []) {
// Only include tool_use blocks — assistant text is model-authored
// and could be crafted to influence the classifier's decision.
if (typeof block !== 'string' && block.type === 'tool_use') {