Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions MLX Code Tests/EditToolTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// EditToolTests.swift
// MLX Code Tests
//
// Unit tests for EditTool: exact-string file edits, uniqueness enforcement,
// multi-edit application, backup creation, changed-line counting, and the
// EditError surface. These exercise the deterministic edit-application logic
// that mutates user files, so correctness here is security/data-integrity
// critical. All I/O is confined to a per-test temp directory.
//
// Created by Jordan Koch.
//

import XCTest
@testable import MLX_Code

final class EditToolTests: XCTestCase {

private var tempDir: URL!

override func setUpWithError() throws {
try super.setUpWithError()
tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("EditToolTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
}

override func tearDownWithError() throws {
if let tempDir = tempDir {
try? FileManager.default.removeItem(at: tempDir)
}
try super.tearDownWithError()
}

// MARK: - Helpers

private func writeFile(_ name: String, _ contents: String) throws -> String {
let path = tempDir.appendingPathComponent(name).path
try contents.write(toFile: path, atomically: true, encoding: .utf8)
return path
}

private func read(_ path: String) throws -> String {
try String(contentsOfFile: path, encoding: .utf8)
}

// MARK: - Single Edit

func testEditReplacesUniqueString() async throws {
let path = try writeFile("a.txt", "hello world\nsecond line\n")

let result = try await EditTool.shared.edit(
filePath: path, oldString: "hello world", newString: "goodbye world")

XCTAssertTrue(result.success)
XCTAssertEqual(result.filePath, path)
XCTAssertEqual(try read(path), "goodbye world\nsecond line\n",
"Only the matched substring should be replaced")
}

func testEditCreatesBackupWithOriginalContents() async throws {
let original = "let x = 1\nlet y = 2\n"
let path = try writeFile("b.swift", original)

let result = try await EditTool.shared.edit(
filePath: path, oldString: "let x = 1", newString: "let x = 42")

XCTAssertFalse(result.backupPath.isEmpty, "A backup path should be returned")
XCTAssertTrue(FileManager.default.fileExists(atPath: result.backupPath),
"Backup file should exist on disk")
XCTAssertEqual(try read(result.backupPath), original,
"Backup should preserve the pre-edit contents exactly")
}

func testEditThrowsWhenFileMissing() async {
let missing = tempDir.appendingPathComponent("does-not-exist.txt").path
do {
_ = try await EditTool.shared.edit(
filePath: missing, oldString: "a", newString: "b")
XCTFail("Editing a missing file should throw")
} catch let error as EditError {
guard case .fileNotFound = error else {
return XCTFail("Expected .fileNotFound, got \(error)")
}
} catch {
XCTFail("Expected EditError, got \(error)")
}
}

func testEditThrowsWhenStringNotFound() async throws {
let path = try writeFile("c.txt", "the quick brown fox")
do {
_ = try await EditTool.shared.edit(
filePath: path, oldString: "lazy dog", newString: "x")
XCTFail("Missing search string should throw")
} catch let error as EditError {
guard case .stringNotFound = error else {
return XCTFail("Expected .stringNotFound, got \(error)")
}
}
XCTAssertEqual(try read(path), "the quick brown fox",
"File must be untouched when the edit fails")
}

func testEditThrowsNotUniqueWhenAmbiguous() async throws {
let path = try writeFile("d.txt", "foo bar foo baz foo")
do {
_ = try await EditTool.shared.edit(
filePath: path, oldString: "foo", newString: "X")
XCTFail("Non-unique oldString without replaceAll should throw")
} catch let error as EditError {
guard case .notUnique(_, let count) = error else {
return XCTFail("Expected .notUnique, got \(error)")
}
XCTAssertEqual(count, 3, "Should report the true occurrence count")
}
XCTAssertEqual(try read(path), "foo bar foo baz foo",
"Ambiguous edit must not mutate the file")
}

func testEditReplaceAllReplacesEveryOccurrence() async throws {
let path = try writeFile("e.txt", "foo foo foo")

let result = try await EditTool.shared.edit(
filePath: path, oldString: "foo", newString: "bar", replaceAll: true)

XCTAssertTrue(result.success)
XCTAssertEqual(try read(path), "bar bar bar",
"replaceAll should replace all matches even when non-unique")
}

func testEditReportsChangedLineCount() async throws {
let path = try writeFile("f.txt", "line1\nline2\nline3\n")

let result = try await EditTool.shared.edit(
filePath: path, oldString: "line2", newString: "CHANGED")

XCTAssertEqual(result.linesChanged, 1,
"Changing content on a single line should count as one changed line")
}

// MARK: - Multi Edit

func testMultiEditAppliesAllEdits() async throws {
let path = try writeFile("g.txt", "alpha beta gamma")

let result = try await EditTool.shared.multiEdit(
filePath: path,
edits: [(old: "alpha", new: "1"), (old: "gamma", new: "3")])

XCTAssertTrue(result.success)
XCTAssertEqual(try read(path), "1 beta 3",
"All provided edits should be applied in order")
}

func testMultiEditIsAtomicOnMissingString() async throws {
let original = "one two three"
let path = try writeFile("h.txt", original)

do {
_ = try await EditTool.shared.multiEdit(
filePath: path,
edits: [(old: "one", new: "1"), (old: "NOPE", new: "x")])
XCTFail("multiEdit should throw when any oldString is absent")
} catch let error as EditError {
guard case .stringNotFound = error else {
return XCTFail("Expected .stringNotFound, got \(error)")
}
}
XCTAssertEqual(try read(path), original,
"No edits should be written if validation fails for any edit")
}

// MARK: - Error Surface

func testEditErrorDescriptions() {
XCTAssertEqual(
EditError.fileNotFound("/x/y").errorDescription,
"File not found: /x/y")
XCTAssertEqual(
EditError.notUnique("abc", 4).errorDescription,
"String appears 4 times (must be unique). Use replaceAll=true or provide more context.")
XCTAssertTrue(
EditError.stringNotFound("needle-in-haystack").errorDescription?
.contains("needle-in-haystack") ?? false,
"stringNotFound description should include (a prefix of) the missing string")
}
}
143 changes: 143 additions & 0 deletions MLX Code Tests/ToolParameterParsingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// ToolParameterParsingTests.swift
// MLX Code Tests
//
// Unit tests for BaseTool's parameter extraction/validation helpers. Every
// concrete tool relies on these to coerce untrusted LLM-supplied argument
// dictionaries into typed values, so their required/default/type-mismatch
// behavior is high-risk and worth pinning down. Also covers the ToolError
// message surface these helpers throw.
//
// Created by Jordan Koch.
//

import XCTest
@testable import MLX_Code

final class ToolParameterParsingTests: XCTestCase {

/// A minimal concrete BaseTool used only to reach the protected helpers.
private func makeTool() -> BaseTool {
BaseTool(
name: "test_tool",
description: "test",
parameters: ToolParameterSchema(properties: [:]))
}

// MARK: - validateParameters

func testValidateParametersPassesWhenPresent() throws {
let tool = makeTool()
XCTAssertNoThrow(
try tool.validateParameters(["a": 1, "b": "x"], required: ["a", "b"]))
}

func testValidateParametersThrowsMissing() {
let tool = makeTool()
XCTAssertThrowsError(
try tool.validateParameters(["a": 1], required: ["a", "b"])
) { error in
guard case ToolError.missingParameter(let name) = error else {
return XCTFail("Expected .missingParameter, got \(error)")
}
XCTAssertEqual(name, "b")
}
}

// MARK: - stringParameter

func testStringParameterReturnsValue() throws {
let tool = makeTool()
XCTAssertEqual(try tool.stringParameter(["k": "hello"], key: "k"), "hello")
}

func testStringParameterUsesDefaultWhenMissing() throws {
let tool = makeTool()
XCTAssertEqual(
try tool.stringParameter([:], key: "k", default: "fallback"), "fallback")
}

func testStringParameterThrowsWhenMissingAndNoDefault() {
let tool = makeTool()
XCTAssertThrowsError(try tool.stringParameter([:], key: "k")) { error in
guard case ToolError.invalidParameterType(let key, let expected) = error else {
return XCTFail("Expected .invalidParameterType, got \(error)")
}
XCTAssertEqual(key, "k")
XCTAssertEqual(expected, "String")
}
}

func testStringParameterThrowsOnWrongType() {
let tool = makeTool()
// An Int value under the key is not a String and no default is provided.
XCTAssertThrowsError(try tool.stringParameter(["k": 123], key: "k"))
}

// MARK: - intParameter

func testIntParameterReturnsValue() throws {
let tool = makeTool()
XCTAssertEqual(try tool.intParameter(["n": 7], key: "n"), 7)
}

func testIntParameterUsesDefault() throws {
let tool = makeTool()
XCTAssertEqual(try tool.intParameter([:], key: "n", default: 42), 42)
}

func testIntParameterThrowsOnWrongTypeWithoutDefault() {
let tool = makeTool()
XCTAssertThrowsError(try tool.intParameter(["n": "notanint"], key: "n")) { error in
guard case ToolError.invalidParameterType = error else {
return XCTFail("Expected .invalidParameterType, got \(error)")
}
}
}

// MARK: - boolParameter

func testBoolParameterReturnsValue() throws {
let tool = makeTool()
XCTAssertTrue(try tool.boolParameter(["flag": true], key: "flag"))
XCTAssertFalse(try tool.boolParameter(["flag": false], key: "flag"))
}

func testBoolParameterUsesDefault() throws {
let tool = makeTool()
XCTAssertTrue(try tool.boolParameter([:], key: "flag", default: true))
}

// MARK: - arrayParameter

func testArrayParameterReturnsValue() throws {
let tool = makeTool()
let result = try tool.arrayParameter(["items": ["a", "b", "c"]], key: "items")
XCTAssertEqual(result.count, 3)
}

func testArrayParameterThrowsOnWrongType() {
let tool = makeTool()
XCTAssertThrowsError(try tool.arrayParameter(["items": "not-an-array"], key: "items")) { error in
guard case ToolError.invalidParameterType(let key, let expected) = error else {
return XCTFail("Expected .invalidParameterType, got \(error)")
}
XCTAssertEqual(key, "items")
XCTAssertEqual(expected, "Array")
}
}

// MARK: - ToolError message surface

func testToolErrorDescriptions() {
XCTAssertEqual(
ToolError.missingParameter("path").errorDescription,
"Missing required parameter: path")
XCTAssertEqual(
ToolError.invalidParameterType("count", expected: "Int").errorDescription,
"Invalid type for parameter 'count': expected Int")
XCTAssertEqual(
ToolError.notFound("/tmp/x").errorDescription,
"Resource not found: /tmp/x")
}
}
Loading