Files
ggbond268--mactools/Plugins/ActivityBar/Tests/ActivityBarHookInstallerTests.swift
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

189 lines
7.2 KiB
Swift

import XCTest
@testable import ActivityBarPlugin
final class ActivityBarHookInstallerTests: XCTestCase {
private var temporaryDirectory: URL!
override func setUpWithError() throws {
temporaryDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent("ActivityBarHookInstallerTests-\(UUID().uuidString)")
try FileManager.default.createDirectory(
at: temporaryDirectory,
withIntermediateDirectories: true
)
}
override func tearDownWithError() throws {
try? FileManager.default.removeItem(at: temporaryDirectory)
}
func testInstallWritesScriptsAndToolConfigurations() throws {
let paths = ActivityBarHookInstallerPaths(
homeDirectory: temporaryDirectory,
hookScriptsDirectory: temporaryDirectory.appendingPathComponent("hooks")
)
let installer = ActivityBarHookInstaller(paths: paths, socketPath: "/tmp/mactools-test.sock")
let summary = try installer.install()
XCTAssertEqual(summary.installedTools, ["Claude Code", "Cursor", "Codex"])
XCTAssertTrue(FileManager.default.fileExists(atPath: paths.hookScriptsDirectory.path))
let claudeScript = paths.hookScriptsDirectory.appendingPathComponent("mactools-activity-claude-hook.sh")
let scriptText = try String(contentsOf: claudeScript, encoding: .utf8)
XCTAssertTrue(scriptText.contains("/tmp/mactools-test.sock"))
let permissions = try FileManager.default.attributesOfItem(atPath: claudeScript.path)[.posixPermissions] as? Int
XCTAssertEqual((permissions ?? 0) & 0o111, 0o111)
let claudeSettings = try readJSONObject(paths.claudeSettingsPath)
let claudeHooks = try XCTUnwrap(claudeSettings["hooks"] as? [String: Any])
XCTAssertNotNil(claudeHooks["UserPromptSubmit"])
let cursorSettings = try readJSONObject(paths.cursorHooksPath)
let cursorHooks = try XCTUnwrap(cursorSettings["hooks"] as? [String: Any])
XCTAssertNotNil(cursorHooks["beforeSubmitPrompt"])
let codexConfig = try String(contentsOf: paths.codexConfigPath, encoding: .utf8)
XCTAssertTrue(codexConfig.contains("codex_hooks = true"))
let codexSettings = try readJSONObject(paths.codexHooksPath)
let codexHooks = try XCTUnwrap(codexSettings["hooks"] as? [String: Any])
XCTAssertNotNil(codexHooks["PreToolUse"])
}
func testInstallIsIdempotentForClaudeHooks() throws {
let paths = ActivityBarHookInstallerPaths(
homeDirectory: temporaryDirectory,
hookScriptsDirectory: temporaryDirectory.appendingPathComponent("hooks")
)
let installer = ActivityBarHookInstaller(paths: paths, socketPath: "/tmp/mactools-test.sock")
_ = try installer.install()
let first = try Data(contentsOf: paths.claudeSettingsPath)
_ = try installer.install()
let second = try Data(contentsOf: paths.claudeSettingsPath)
XCTAssertEqual(first, second)
}
func testUninstallRemovesOnlyActivityBarHooks() throws {
let paths = ActivityBarHookInstallerPaths(
homeDirectory: temporaryDirectory,
hookScriptsDirectory: temporaryDirectory.appendingPathComponent("hooks")
)
try writeJSONObject(
[
"hooks": [
"UserPromptSubmit": [
[
"matcher": "",
"hooks": [
[
"type": "command",
"command": "/usr/bin/true",
]
],
]
],
],
],
to: paths.claudeSettingsPath
)
try writeJSONObject(
[
"version": 1,
"hooks": [
"beforeSubmitPrompt": [
[
"command": "/usr/bin/true",
]
],
],
],
to: paths.cursorHooksPath
)
try writeJSONObject(
[
"hooks": [
"PreToolUse": [
[
"matcher": "",
"hooks": [
[
"type": "command",
"command": "/usr/bin/true",
]
],
]
],
],
],
to: paths.codexHooksPath
)
let installer = ActivityBarHookInstaller(paths: paths, socketPath: "/tmp/mactools-test.sock")
_ = try installer.install()
let summary = try installer.uninstall()
XCTAssertEqual(summary.removedTools, ["Claude Code", "Cursor", "Codex"])
XCTAssertFalse(FileManager.default.fileExists(atPath: paths.hookScriptsDirectory.path))
let claudeObject = try readJSONObject(paths.claudeSettingsPath)
let cursorObject = try readJSONObject(paths.cursorHooksPath)
let codexObject = try readJSONObject(paths.codexHooksPath)
let claudeSettings = try serializedJSONObject(claudeObject)
let cursorSettings = try serializedJSONObject(cursorObject)
let codexSettings = try serializedJSONObject(codexObject)
XCTAssertFalse(claudeSettings.contains("mactools-activity"))
XCTAssertFalse(cursorSettings.contains("mactools-activity"))
XCTAssertFalse(codexSettings.contains("mactools-activity"))
XCTAssertTrue(containsString("/usr/bin/true", in: claudeObject))
XCTAssertTrue(containsString("/usr/bin/true", in: cursorObject))
XCTAssertTrue(containsString("/usr/bin/true", in: codexObject))
}
private func readJSONObject(_ url: URL) throws -> [String: Any] {
let data = try Data(contentsOf: url)
return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
}
private func serializedJSONObject(_ object: [String: Any]) throws -> String {
let data = try JSONSerialization.data(
withJSONObject: object,
options: [.sortedKeys]
)
return try XCTUnwrap(String(data: data, encoding: .utf8))
}
private func writeJSONObject(_ object: [String: Any], to url: URL) throws {
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let data = try JSONSerialization.data(
withJSONObject: object,
options: [.prettyPrinted, .sortedKeys]
)
try data.write(to: url, options: .atomic)
}
private func containsString(_ expected: String, in object: Any) -> Bool {
if let value = object as? String {
return value == expected
}
if let values = object as? [Any] {
return values.contains { containsString(expected, in: $0) }
}
if let values = object as? [String: Any] {
return values.values.contains { containsString(expected, in: $0) }
}
return false
}
}