Download apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatToolDiffTests.swift from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatToolDiffTests.swift
- Command line
-
hf download hf://SaylorTwift/openclaw/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatToolDiffTests.swift
-
curl -L -o ChatToolDiffTests.swift https://huggingface.co/SaylorTwift/openclaw/resolve/main/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatToolDiffTests.swift
18.5 kB
| import Foundation | |
| import OpenClawKit | |
| import Testing | |
| @testable import OpenClawChatUI | |
| ("ChatToolDiff") | |
| struct ChatToolDiffTests { | |
| private func parsedLines(_ diff: String) -> [ChatToolDiffLine]? { | |
| ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: nil, | |
| details: AnyCodable(["diff": AnyCodable(diff)]))?.lines | |
| } | |
| func `parses numbered details diffs`() { | |
| let parsed = self.parsedLines(" 455 before\n-456 old\n+456 new") | |
| #expect(parsed == [ | |
| ChatToolDiffLine(kind: .ctx, lineNo: 455, text: "before"), | |
| ChatToolDiffLine(kind: .del, lineNo: 456, text: "old"), | |
| ChatToolDiffLine(kind: .add, lineNo: 456, text: "new"), | |
| ]) | |
| } | |
| func `parses skip markers`() { | |
| let cases = ["+1 kept\n...", "+1 kept\n...(truncated)..."] | |
| for diff in cases { | |
| #expect(self.parsedLines(diff) == [ | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "kept"), | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ]) | |
| } | |
| } | |
| func `rejects malformed or unchanged details`() { | |
| for diff in ["raw line", " 1 context\n 2 context"] { | |
| #expect(self.parsedLines(diff) == nil) | |
| } | |
| } | |
| func `computes basic line diffs`() { | |
| let cases: [(String, String, [ChatToolDiffLine])] = [ | |
| ("before", "after", [ | |
| ChatToolDiffLine(kind: .del, text: "before"), | |
| ChatToolDiffLine(kind: .add, text: "after"), | |
| ]), | |
| ("same", "same\nadded", [ | |
| ChatToolDiffLine(kind: .ctx, text: "same"), | |
| ChatToolDiffLine(kind: .add, text: "added"), | |
| ]), | |
| ("same\nremoved", "same", [ | |
| ChatToolDiffLine(kind: .ctx, text: "same"), | |
| ChatToolDiffLine(kind: .del, text: "removed"), | |
| ]), | |
| ("", "written", [ChatToolDiffLine(kind: .add, text: "written")]), | |
| ] | |
| for (old, new, expected) in cases { | |
| #expect(ChatToolDiff.computeLineDiff(old: old, new: new) == expected) | |
| } | |
| } | |
| func `trailing newline is not an extra line`() { | |
| #expect(ChatToolDiff.computeLineDiff(old: "", new: "foo\n") == [ | |
| ChatToolDiffLine(kind: .add, text: "foo"), | |
| ]) | |
| } | |
| func `oversized input degrades to a skip`() { | |
| let text = (0...600).map { "line \($0)" }.joined(separator: "\n") | |
| #expect(ChatToolDiff.computeLineDiff(old: text, new: text) == [ | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ]) | |
| } | |
| func `compaction keeps three context lines around a change`() { | |
| let oldLines = (0..<500).map { "line \($0)" } | |
| var newLines = oldLines | |
| newLines[250] = "changed" | |
| let expected = [ | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ChatToolDiffLine(kind: .ctx, text: "line 247"), | |
| ChatToolDiffLine(kind: .ctx, text: "line 248"), | |
| ChatToolDiffLine(kind: .ctx, text: "line 249"), | |
| ChatToolDiffLine(kind: .del, text: "line 250"), | |
| ChatToolDiffLine(kind: .add, text: "changed"), | |
| ChatToolDiffLine(kind: .ctx, text: "line 251"), | |
| ChatToolDiffLine(kind: .ctx, text: "line 252"), | |
| ChatToolDiffLine(kind: .ctx, text: "line 253"), | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ] | |
| #expect(ChatToolDiff.computeLineDiff( | |
| old: oldLines.joined(separator: "\n"), | |
| new: newLines.joined(separator: "\n")) == expected) | |
| } | |
| func `details diff wins over argument fallback`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: AnyCodable(["oldText": "arg old", "newText": "arg new"]), | |
| details: AnyCodable(["diff": "-12 detail old\n+12 detail new"]))) | |
| #expect(resolved.lines == [ | |
| ChatToolDiffLine(kind: .del, lineNo: 12, text: "detail old"), | |
| ChatToolDiffLine(kind: .add, lineNo: 12, text: "detail new"), | |
| ]) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 1, removed: 1)) | |
| } | |
| func `bare details skip keeps an exact stat`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: nil, | |
| details: AnyCodable(["diff": "+1 added\n...\n-2 removed"]))) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 1, removed: 1)) | |
| } | |
| func `reads multi edit pairs`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "multiedit", | |
| arguments: AnyCodable(["edits": [ | |
| ["oldText": "one", "newText": "uno"], | |
| ["old_string": "two", "new_string": "dos"], | |
| ]]), | |
| details: nil)) | |
| #expect(resolved.lines == [ | |
| ChatToolDiffLine(kind: .del, text: "one"), | |
| ChatToolDiffLine(kind: .add, text: "uno"), | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ChatToolDiffLine(kind: .del, text: "two"), | |
| ChatToolDiffLine(kind: .add, text: "dos"), | |
| ]) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 2, removed: 2)) | |
| } | |
| func `renders write content as additions`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "write_file", | |
| arguments: AnyCodable(["content": "one\ntwo\n"]), | |
| details: nil)) | |
| #expect(resolved.lines == [ | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "one"), | |
| ChatToolDiffLine(kind: .add, lineNo: 2, text: "two"), | |
| ]) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 2, removed: 0)) | |
| } | |
| func `write created flags control fallback stats`() throws { | |
| let arguments = AnyCodable(["content": "one\ntwo\n"]) | |
| let created = try #require(ChatToolDiff.resolveDiff( | |
| name: "write", | |
| arguments: arguments, | |
| details: AnyCodable(["created": true]))) | |
| let overwritten = try #require(ChatToolDiff.resolveDiff( | |
| name: "write", | |
| arguments: arguments, | |
| details: AnyCodable(["created": false]))) | |
| let unknown = try #require(ChatToolDiff.resolveDiff( | |
| name: "write", | |
| arguments: arguments, | |
| details: AnyCodable(["changed": true]))) | |
| #expect(created.stat == ChatToolDiffStat(added: 2, removed: 0)) | |
| #expect(overwritten.lines == created.lines) | |
| #expect(overwritten.stat == nil) | |
| #expect(unknown.lines == created.lines) | |
| #expect(unknown.stat == nil) | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "write", | |
| arguments: arguments, | |
| details: AnyCodable(["changed": false])) == nil) | |
| } | |
| func `renders editor create and insert commands`() throws { | |
| let create = try #require(ChatToolDiff.resolveDiff( | |
| name: "str_replace_editor", | |
| arguments: AnyCodable(["command": "create", "file_text": "one\ntwo"]), | |
| details: nil)) | |
| #expect(create.lines == [ | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "one"), | |
| ChatToolDiffLine(kind: .add, lineNo: 2, text: "two"), | |
| ]) | |
| #expect(create.stat == ChatToolDiffStat(added: 2, removed: 0)) | |
| let createFallback = try #require(ChatToolDiff.resolveDiff( | |
| name: "str_replace_editor", | |
| arguments: AnyCodable(["command": "create", "content": "fallback"]), | |
| details: nil)) | |
| #expect(createFallback.lines == [ChatToolDiffLine(kind: .add, lineNo: 1, text: "fallback")]) | |
| #expect(createFallback.stat == ChatToolDiffStat(added: 1, removed: 0)) | |
| let overwrittenCreate = try #require(ChatToolDiff.resolveDiff( | |
| name: "str_replace_editor", | |
| arguments: AnyCodable(["command": "create", "file_text": "replacement"]), | |
| details: AnyCodable(["created": false]))) | |
| #expect(overwrittenCreate.stat == nil) | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "str_replace_editor", | |
| arguments: AnyCodable(["command": "create", "file_text": "unchanged"]), | |
| details: AnyCodable(["changed": false])) == nil) | |
| let insert = try #require(ChatToolDiff.resolveDiff( | |
| name: "str_replace_based_edit_tool", | |
| arguments: AnyCodable(["command": "insert", "insert_text": "three"]), | |
| details: nil)) | |
| #expect(insert.lines == [ChatToolDiffLine(kind: .add, text: "three")]) | |
| #expect(insert.stat == nil) | |
| } | |
| func `keeps a full write stat when the preview is clipped`() throws { | |
| let content = (0...400).map { "line \($0)" }.joined(separator: "\n") | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "write", | |
| arguments: AnyCodable(["content": content]), | |
| details: nil)) | |
| #expect(resolved.lines.count == 81) | |
| #expect(resolved.lines.last == ChatToolDiffLine(kind: .skip, text: "")) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 401, removed: 0)) | |
| } | |
| func `unknown tools do not resolve local diffs`() { | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "custom_tool", | |
| arguments: AnyCodable(["oldText": "old", "newText": "new"]), | |
| details: nil) == nil) | |
| } | |
| func `unknown tools never interpret details as a diff`() { | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "custom_tool", | |
| arguments: nil, | |
| details: AnyCodable(["diff": AnyCodable("+1 added\n-1 removed")])) == nil) | |
| } | |
| func `move only patches render the file header`() throws { | |
| let patch = "*** Begin Patch\n*** Update File: a/old.txt\n*** Move to: a/new.txt\n*** End Patch" | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable(["input": AnyCodable(patch)]), | |
| details: nil)) | |
| #expect(resolved.lines == [ChatToolDiffLine(kind: .file, text: "Move a/old.txt → a/new.txt")]) | |
| #expect(resolved.stat == nil) | |
| } | |
| func `patch tools resolve persisted details`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: nil, | |
| details: AnyCodable(["diff": AnyCodable("+1 added\n-1 removed")]))) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 1, removed: 1)) | |
| } | |
| func `parses numbered update patch envelopes from every argument spelling`() throws { | |
| let patch = [ | |
| "*** Begin Patch", | |
| "*** Update File: src/a.swift", | |
| "@@ -4,2 +4,2 @@", | |
| " context", | |
| "-old", | |
| "+new", | |
| "*** End Patch", | |
| ].joined(separator: "\n") | |
| for key in ["patch", "input", "diff"] { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable([key: AnyCodable(patch)]), | |
| details: nil)) | |
| #expect(resolved.lines == [ | |
| ChatToolDiffLine(kind: .ctx, lineNo: 4, text: "context"), | |
| ChatToolDiffLine(kind: .del, lineNo: 5, text: "old"), | |
| ChatToolDiffLine(kind: .add, lineNo: 5, text: "new"), | |
| ]) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 1, removed: 1)) | |
| } | |
| } | |
| func `patch envelope precedence matches transcript caching`() throws { | |
| let input = "*** Begin Patch\n*** Add File: input.txt\n+input\n*** End Patch" | |
| let patch = "*** Begin Patch\n*** Add File: patch.txt\n+patch\n*** End Patch" | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable([ | |
| "input": AnyCodable(input), | |
| "patch": AnyCodable(patch), | |
| ]), | |
| details: nil)) | |
| #expect(resolved.lines.first == ChatToolDiffLine(kind: .add, lineNo: 1, text: "input")) | |
| } | |
| func `separates add delete and move patch files`() throws { | |
| let patch = [ | |
| "*** Begin Patch", | |
| "*** Update File: src/old.swift", | |
| "*** Move to: src/new.swift", | |
| "@@ -1 +1 @@", | |
| "-old", | |
| "+new", | |
| "*** Add File: src/added.swift", | |
| "+added", | |
| "*** Delete File: src/deleted.swift", | |
| "-deleted", | |
| "*** End Patch", | |
| ].joined(separator: "\n") | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "applypatch", | |
| arguments: AnyCodable(["input": AnyCodable(patch)]), | |
| details: nil)) | |
| #expect(resolved.lines == [ | |
| ChatToolDiffLine(kind: .file, text: "Move src/old.swift → src/new.swift"), | |
| ChatToolDiffLine(kind: .del, lineNo: 1, text: "old"), | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "new"), | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ChatToolDiffLine(kind: .file, text: "Add src/added.swift"), | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "added"), | |
| ChatToolDiffLine(kind: .skip, text: ""), | |
| ChatToolDiffLine(kind: .file, text: "Delete src/deleted.swift"), | |
| ChatToolDiffLine(kind: .del, lineNo: 1, text: "deleted"), | |
| ]) | |
| #expect(resolved.stat == ChatToolDiffStat(added: 2, removed: 2)) | |
| } | |
| func `rejects malformed patch envelopes`() { | |
| for patch in [ | |
| "*** Begin Patch\nnot a file\n*** End Patch", | |
| "*** Update File: \n+orphaned", | |
| "*** Add File: empty.swift\n*** End Patch", | |
| ] { | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "patch", | |
| arguments: AnyCodable(["patch": AnyCodable(patch)]), | |
| details: nil) == nil) | |
| } | |
| } | |
| func `renders header only deletes without an exact stat`() throws { | |
| let deleted = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable(["patch": AnyCodable("*** Delete File: obsolete.swift")]), | |
| details: nil)) | |
| #expect(deleted.lines == [ChatToolDiffLine(kind: .file, text: "Delete obsolete.swift")]) | |
| #expect(deleted.stat == nil) | |
| let multi = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable(["patch": AnyCodable( | |
| "*** Add File: added.swift\n+new\n*** Delete File: obsolete.swift")]), | |
| details: nil)) | |
| #expect(multi.lines.last == ChatToolDiffLine(kind: .file, text: "Delete obsolete.swift")) | |
| #expect(multi.stat == nil) | |
| } | |
| func `caps patch rows and omits a partial stat`() throws { | |
| let patch = (["*** Begin Patch", "*** Update File: big.swift"] + | |
| (0..<450).map { "+line \($0)" } + ["*** End Patch"]) | |
| .joined(separator: "\n") | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "apply_patch", | |
| arguments: AnyCodable(["patch": AnyCodable(patch)]), | |
| details: nil)) | |
| #expect(resolved.lines.count == 401) | |
| #expect(resolved.lines.last == ChatToolDiffLine(kind: .skip, text: "")) | |
| #expect(resolved.stat == nil) | |
| } | |
| func `failed edits suppress argument proposals but keep applied details`() throws { | |
| #expect(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: AnyCodable(["oldText": "old", "newText": "new"]), | |
| details: nil, | |
| isError: true) == nil) | |
| let details = try #require(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: AnyCodable(["oldText": "arg old", "newText": "arg new"]), | |
| details: AnyCodable(["diff": AnyCodable("-1 applied old\n+1 applied new")]), | |
| isError: true)) | |
| #expect(details.lines == [ | |
| ChatToolDiffLine(kind: .del, lineNo: 1, text: "applied old"), | |
| ChatToolDiffLine(kind: .add, lineNo: 1, text: "applied new"), | |
| ]) | |
| #expect(details.stat == ChatToolDiffStat(added: 1, removed: 1)) | |
| } | |
| func `truncated details omit the stat`() throws { | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: nil, | |
| details: AnyCodable(["diff": "+1 kept\n...(truncated)..."]))) | |
| #expect(resolved.stat == nil) | |
| } | |
| func `capped details omit the stat`() throws { | |
| let diff = (1...401).map { "+\($0) line \($0)" }.joined(separator: "\n") | |
| let resolved = try #require(ChatToolDiff.resolveDiff( | |
| name: "edit", | |
| arguments: nil, | |
| details: AnyCodable(["diff": diff]))) | |
| #expect(resolved.lines.count == 401) | |
| #expect(resolved.lines.last == ChatToolDiffLine(kind: .skip, text: "")) | |
| #expect(resolved.stat == nil) | |
| } | |
| func `message details survive coding roundtrip`() throws { | |
| let data = Data(#"{"role":"toolResult","content":"done","details":{"diff":"+1 added"}}"#.utf8) | |
| let decoded = try JSONDecoder().decode(OpenClawChatMessage.self, from: data) | |
| let roundTripped = try JSONDecoder().decode( | |
| OpenClawChatMessage.self, | |
| from: JSONEncoder().encode(decoded)) | |
| #expect(decoded.details == AnyCodable(["diff": AnyCodable("+1 added")])) | |
| #expect(roundTripped.details == decoded.details) | |
| } | |
| func `message and content decode snake case errors and encode canonically`() throws { | |
| let data = Data(#"{"role":"toolResult","is_error":true,"content":[{"type":"tool_result","is_error":true}]}"# | |
| .utf8) | |
| let decoded = try JSONDecoder().decode(OpenClawChatMessage.self, from: data) | |
| let encoded = try JSONEncoder().encode(decoded) | |
| let object = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) | |
| let content = try #require((object["content"] as? [[String: Any]])?.first) | |
| #expect(decoded.isError == true) | |
| #expect(decoded.content.first?.isError == true) | |
| #expect(object["isError"] as? Bool == true) | |
| #expect(object["is_error"] == nil) | |
| #expect(content["isError"] as? Bool == true) | |
| #expect(content["is_error"] == nil) | |
| } | |
| } | |