From f4eebb28174905480044963a5713e270dcc9e52c Mon Sep 17 00:00:00 2001 From: Lee Rosen <96027741+tsconfigdotjson@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:15:56 -0400 Subject: [PATCH] Remove skills-lock.json and fix all SwiftLint CI violations - Remove skills-lock.json from repo - Split ANSIParserTests into two files to stay under file/type length limits - Unnest SavedAuxCommand in PersistenceTests to fix nesting violation - Use Data(_:) initializer instead of .data(using:) in tests - Break long JSON test line under 120 chars - Disable redundant_discardable_let rule (required in SwiftUI ViewBuilder) Co-Authored-By: Claude Opus 4.6 (1M context) --- .swiftlint.yml | 1 + .../ANSIParserEdgeCaseTests.swift | 266 ++++++++++++++++++ Tests/DevtailKitTests/ANSIParserTests.swift | 248 ---------------- Tests/DevtailKitTests/PersistenceTests.swift | 34 +-- skills-lock.json | 10 - 5 files changed, 285 insertions(+), 274 deletions(-) create mode 100644 Tests/DevtailKitTests/ANSIParserEdgeCaseTests.swift delete mode 100644 skills-lock.json diff --git a/.swiftlint.yml b/.swiftlint.yml index 8d61f44..6c593cb 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -13,5 +13,6 @@ identifier_name: disabled_rules: - trailing_comma - opening_brace + - redundant_discardable_let excluded: - .build diff --git a/Tests/DevtailKitTests/ANSIParserEdgeCaseTests.swift b/Tests/DevtailKitTests/ANSIParserEdgeCaseTests.swift new file mode 100644 index 0000000..9d2c853 --- /dev/null +++ b/Tests/DevtailKitTests/ANSIParserEdgeCaseTests.swift @@ -0,0 +1,266 @@ +import Testing + +@testable import DevtailKit + +struct ANSIParserEdgeCaseTests { + + private func parse(_ input: String) -> [TerminalAction] { + var parser = ANSIParser() + return parser.parse(input) + } + + private func firstSpan(_ actions: [TerminalAction]) -> StyledSpan? { + for action in actions { + if case .text(let span) = action { return span } + } + return nil + } + + private func allSpans(_ actions: [TerminalAction]) -> [StyledSpan] { + actions.compactMap { + if case .text(let span) = $0 { return span } + return nil + } + } + + @Test func eraseLineCode2K() { + let actions = parse("\u{1B}[2K") + #expect(actions.count == 1) + guard case .eraseLine = actions[0] else { + Issue.record("Expected .eraseLine") + return + } + } + + @Test func eraseToEndOfLineCodeK() { + let actions = parse("\u{1B}[K") + #expect(actions.count == 1) + guard case .eraseToEndOfLine = actions[0] else { + Issue.record("Expected .eraseToEndOfLine") + return + } + } + + @Test func eraseToEndOfLineCode0K() { + let actions = parse("\u{1B}[0K") + #expect(actions.count == 1) + guard case .eraseToEndOfLine = actions[0] else { + Issue.record("Expected .eraseToEndOfLine") + return + } + } + + @Test func cursorUpDefault() { + let actions = parse("\u{1B}[A") + #expect(actions.count == 1) + guard case .cursorUp(let n) = actions[0] else { + Issue.record("Expected .cursorUp") + return + } + #expect(n == 1) + } + + @Test func cursorUpExplicitCount() { + let actions = parse("\u{1B}[3A") + #expect(actions.count == 1) + guard case .cursorUp(let n) = actions[0] else { + Issue.record("Expected .cursorUp") + return + } + #expect(n == 3) + } + + @Test func cursorUpZeroBecomesOne() { + let actions = parse("\u{1B}[0A") + guard case .cursorUp(let n) = actions[0] else { + Issue.record("Expected .cursorUp") + return + } + #expect(n == 1) + } + + @Test func multipleSGRParams() { + let actions = parse("\u{1B}[1;31;42mCombined") + let span = firstSpan(actions) + #expect(span?.style.bold == true) + #expect(span?.style.foreground == .standard(1)) + #expect(span?.style.background == .standard(2)) + #expect(span?.text == "Combined") + } + + @Test func multipleSGRParamsItalicBrightCyan() { + let actions = parse("\u{1B}[3;96mTest") + let span = firstSpan(actions) + #expect(span?.style.italic == true) + #expect(span?.style.foreground == .bright(6)) + } + + @Test func controlCharactersAreFiltered() { + let input = "A\u{01}B\u{02}C\u{07}D" + let actions = parse(input) + let span = firstSpan(actions) + #expect(span?.text == "ABCD") + } + + @Test func tabCharacterPassesThrough() { + let actions = parse("A\tB") + let span = firstSpan(actions) + #expect(span?.text == "A\tB") + } + + @Test func incompleteEscapeSequenceIsSkipped() { + let actions = parse("Hello\u{1B}") + #expect(actions.count == 1) + let span = firstSpan(actions) + #expect(span?.text == "Hello") + } + + @Test func escNotFollowedByBracketSkips() { + let actions = parse("A\u{1B}XB") + let spans = allSpans(actions) + let combined = spans.map(\.text).joined() + #expect(combined == "AXB") + } + + @Test func incompleteCSISequenceIsSkipped() { + let actions = parse("Hi\u{1B}[31") + let span = firstSpan(actions) + #expect(span?.text == "Hi") + } + + @Test func stylePersistsAcrossParseCalls() { + var parser = ANSIParser() + _ = parser.parse("\u{1B}[1;31m") + let actions = parser.parse("StyledText") + let span = firstSpan(actions) + #expect(span?.style.bold == true) + #expect(span?.style.foreground == .standard(1)) + #expect(span?.text == "StyledText") + } + + @Test func stylePersistsThenResets() { + var parser = ANSIParser() + let a1 = parser.parse("\u{1B}[32mGreen") + let s1 = firstSpan(a1) + #expect(s1?.style.foreground == .standard(2)) + + let a2 = parser.parse("\u{1B}[0mPlain") + let s2 = firstSpan(a2) + #expect(s2?.style == ANSIStyle()) + } + + @Test func mixedTextAndEscapes() { + let actions = parse("Hello \u{1B}[31mWorld\u{1B}[0m!") + let spans = allSpans(actions) + #expect(spans.count == 3) + #expect(spans[0].text == "Hello ") + #expect(spans[0].style.foreground == .default) + #expect(spans[1].text == "World") + #expect(spans[1].style.foreground == .standard(1)) + #expect(spans[2].text == "!") + #expect(spans[2].style == ANSIStyle()) + } + + @Test func escapeSequenceAtStartOfString() { + let actions = parse("\u{1B}[34mBlue text") + let span = firstSpan(actions) + #expect(span?.style.foreground == .standard(4)) + #expect(span?.text == "Blue text") + } + + @Test func escapeSequenceAtEndOfString() { + let actions = parse("Text\u{1B}[0m") + let spans = allSpans(actions) + #expect(spans.count == 1) + #expect(spans[0].text == "Text") + } + + @Test func multipleNewlinesInSequence() { + let actions = parse("\n\n\n") + #expect(actions.count == 3) + for action in actions { + guard case .newline = action else { + Issue.record("Expected all .newline") + return + } + } + } + + @Test func separateCRAndLFProduceBothActions() { + var parser = ANSIParser() + let a1 = parser.parse("line1\r") + let a2 = parser.parse("\nline2") + #expect(a1.count == 2) + if case .text(let s) = a1[0] { #expect(s.text == "line1") } + guard case .carriageReturn = a1[1] else { + Issue.record("Expected .carriageReturn") + return + } + #expect(a2.count == 2) + guard case .newline = a2[0] else { + Issue.record("Expected .newline") + return + } + if case .text(let s) = a2[1] { #expect(s.text == "line2") } + } + + @Test func carriageReturnFollowedByText() { + let actions = parse("old\rnew") + #expect(actions.count == 3) + if case .text(let s) = actions[0] { #expect(s.text == "old") } + guard case .carriageReturn = actions[1] else { + Issue.record("Expected .carriageReturn") + return + } + if case .text(let s) = actions[2] { #expect(s.text == "new") } + } + + @Test func extendedColor38WithInsufficientParams() { + let actions = parse("\u{1B}[38;5mX") + let span = firstSpan(actions) + #expect(span?.text == "X") + } + + @Test func extendedColor38ModeUnknown() { + let actions = parse("\u{1B}[38;3;100mX") + let span = firstSpan(actions) + #expect(span?.text == "X") + #expect(span?.style.foreground == .default) + } + + @Test func truecolorWithInsufficientParams() { + let actions = parse("\u{1B}[38;2;255;128mX") + let span = firstSpan(actions) + #expect(span?.text == "X") + #expect(span?.style.foreground == .default) + } + + @Test func unrecognizedCSIFinalCharIsIgnored() { + let actions = parse("A\u{1B}[2JB") + let spans = allSpans(actions) + let combined = spans.map(\.text).joined() + #expect(combined == "AB") + } + + @Test func npmColoredOutput() { + let input = "\u{1B}[1m\u{1B}[32m>\u{1B}[0m dev\n next dev" + let actions = parse(input) + let spans = allSpans(actions) + #expect(spans.count >= 2) + #expect(spans[0].style.bold == true) + #expect(spans[0].style.foreground == .standard(2)) + #expect(spans[0].text == ">") + } + + @Test func progressBarWithCR() { + let input = "Progress: 50%\rProgress: 100%" + let actions = parse(input) + #expect(actions.count == 3) + if case .text(let s) = actions[0] { #expect(s.text == "Progress: 50%") } + guard case .carriageReturn = actions[1] else { + Issue.record("Expected .carriageReturn") + return + } + if case .text(let s) = actions[2] { #expect(s.text == "Progress: 100%") } + } +} diff --git a/Tests/DevtailKitTests/ANSIParserTests.swift b/Tests/DevtailKitTests/ANSIParserTests.swift index 8278c98..b3658cf 100644 --- a/Tests/DevtailKitTests/ANSIParserTests.swift +++ b/Tests/DevtailKitTests/ANSIParserTests.swift @@ -16,13 +16,6 @@ struct ANSIParserTests { return nil } - private func allSpans(_ actions: [TerminalAction]) -> [StyledSpan] { - actions.compactMap { - if case .text(let span) = $0 { return span } - return nil - } - } - @Test func plainTextPassesThrough() { let actions = parse("hello world") #expect(actions.count == 1) @@ -252,245 +245,4 @@ struct ANSIParserTests { let span = firstSpan(actions) #expect(span?.style.background == .default) } - - @Test func eraseLineCode2K() { - let actions = parse("\u{1B}[2K") - #expect(actions.count == 1) - guard case .eraseLine = actions[0] else { - Issue.record("Expected .eraseLine") - return - } - } - - @Test func eraseToEndOfLineCodeK() { - let actions = parse("\u{1B}[K") - #expect(actions.count == 1) - guard case .eraseToEndOfLine = actions[0] else { - Issue.record("Expected .eraseToEndOfLine") - return - } - } - - @Test func eraseToEndOfLineCode0K() { - let actions = parse("\u{1B}[0K") - #expect(actions.count == 1) - guard case .eraseToEndOfLine = actions[0] else { - Issue.record("Expected .eraseToEndOfLine") - return - } - } - - @Test func cursorUpDefault() { - let actions = parse("\u{1B}[A") - #expect(actions.count == 1) - guard case .cursorUp(let n) = actions[0] else { - Issue.record("Expected .cursorUp") - return - } - #expect(n == 1) - } - - @Test func cursorUpExplicitCount() { - let actions = parse("\u{1B}[3A") - #expect(actions.count == 1) - guard case .cursorUp(let n) = actions[0] else { - Issue.record("Expected .cursorUp") - return - } - #expect(n == 3) - } - - @Test func cursorUpZeroBecomesOne() { - let actions = parse("\u{1B}[0A") - guard case .cursorUp(let n) = actions[0] else { - Issue.record("Expected .cursorUp") - return - } - #expect(n == 1) - } - - @Test func multipleSGRParams() { - let actions = parse("\u{1B}[1;31;42mCombined") - let span = firstSpan(actions) - #expect(span?.style.bold == true) - #expect(span?.style.foreground == .standard(1)) - #expect(span?.style.background == .standard(2)) - #expect(span?.text == "Combined") - } - - @Test func multipleSGRParamsItalicBrightCyan() { - let actions = parse("\u{1B}[3;96mTest") - let span = firstSpan(actions) - #expect(span?.style.italic == true) - #expect(span?.style.foreground == .bright(6)) - } - - @Test func controlCharactersAreFiltered() { - let input = "A\u{01}B\u{02}C\u{07}D" - let actions = parse(input) - let span = firstSpan(actions) - #expect(span?.text == "ABCD") - } - - @Test func tabCharacterPassesThrough() { - let actions = parse("A\tB") - let span = firstSpan(actions) - #expect(span?.text == "A\tB") - } - - @Test func incompleteEscapeSequenceIsSkipped() { - let actions = parse("Hello\u{1B}") - #expect(actions.count == 1) - let span = firstSpan(actions) - #expect(span?.text == "Hello") - } - - @Test func escNotFollowedByBracketSkips() { - let actions = parse("A\u{1B}XB") - let spans = allSpans(actions) - let combined = spans.map(\.text).joined() - #expect(combined == "AXB") - } - - @Test func incompleteCSISequenceIsSkipped() { - let actions = parse("Hi\u{1B}[31") - let span = firstSpan(actions) - #expect(span?.text == "Hi") - } - - @Test func stylePersistsAcrossParseCalls() { - var parser = ANSIParser() - _ = parser.parse("\u{1B}[1;31m") - let actions = parser.parse("StyledText") - let span = firstSpan(actions) - #expect(span?.style.bold == true) - #expect(span?.style.foreground == .standard(1)) - #expect(span?.text == "StyledText") - } - - @Test func stylePersistsThenResets() { - var parser = ANSIParser() - let a1 = parser.parse("\u{1B}[32mGreen") - let s1 = firstSpan(a1) - #expect(s1?.style.foreground == .standard(2)) - - let a2 = parser.parse("\u{1B}[0mPlain") - let s2 = firstSpan(a2) - #expect(s2?.style == ANSIStyle()) - } - - @Test func mixedTextAndEscapes() { - let actions = parse("Hello \u{1B}[31mWorld\u{1B}[0m!") - let spans = allSpans(actions) - #expect(spans.count == 3) - #expect(spans[0].text == "Hello ") - #expect(spans[0].style.foreground == .default) - #expect(spans[1].text == "World") - #expect(spans[1].style.foreground == .standard(1)) - #expect(spans[2].text == "!") - #expect(spans[2].style == ANSIStyle()) - } - - @Test func escapeSequenceAtStartOfString() { - let actions = parse("\u{1B}[34mBlue text") - let span = firstSpan(actions) - #expect(span?.style.foreground == .standard(4)) - #expect(span?.text == "Blue text") - } - - @Test func escapeSequenceAtEndOfString() { - let actions = parse("Text\u{1B}[0m") - let spans = allSpans(actions) - #expect(spans.count == 1) - #expect(spans[0].text == "Text") - } - - @Test func multipleNewlinesInSequence() { - let actions = parse("\n\n\n") - #expect(actions.count == 3) - for action in actions { - guard case .newline = action else { - Issue.record("Expected all .newline") - return - } - } - } - - @Test func separateCRAndLFProduceBothActions() { - var parser = ANSIParser() - let a1 = parser.parse("line1\r") - let a2 = parser.parse("\nline2") - #expect(a1.count == 2) - if case .text(let s) = a1[0] { #expect(s.text == "line1") } - guard case .carriageReturn = a1[1] else { - Issue.record("Expected .carriageReturn") - return - } - #expect(a2.count == 2) - guard case .newline = a2[0] else { - Issue.record("Expected .newline") - return - } - if case .text(let s) = a2[1] { #expect(s.text == "line2") } - } - - @Test func carriageReturnFollowedByText() { - let actions = parse("old\rnew") - #expect(actions.count == 3) - if case .text(let s) = actions[0] { #expect(s.text == "old") } - guard case .carriageReturn = actions[1] else { - Issue.record("Expected .carriageReturn") - return - } - if case .text(let s) = actions[2] { #expect(s.text == "new") } - } - - @Test func extendedColor38WithInsufficientParams() { - let actions = parse("\u{1B}[38;5mX") - let span = firstSpan(actions) - #expect(span?.text == "X") - } - - @Test func extendedColor38ModeUnknown() { - let actions = parse("\u{1B}[38;3;100mX") - let span = firstSpan(actions) - #expect(span?.text == "X") - #expect(span?.style.foreground == .default) - } - - @Test func truecolorWithInsufficientParams() { - let actions = parse("\u{1B}[38;2;255;128mX") - let span = firstSpan(actions) - #expect(span?.text == "X") - #expect(span?.style.foreground == .default) - } - - @Test func unrecognizedCSIFinalCharIsIgnored() { - let actions = parse("A\u{1B}[2JB") - let spans = allSpans(actions) - let combined = spans.map(\.text).joined() - #expect(combined == "AB") - } - - @Test func npmColoredOutput() { - let input = "\u{1B}[1m\u{1B}[32m>\u{1B}[0m dev\n next dev" - let actions = parse(input) - let spans = allSpans(actions) - #expect(spans.count >= 2) - #expect(spans[0].style.bold == true) - #expect(spans[0].style.foreground == .standard(2)) - #expect(spans[0].text == ">") - } - - @Test func progressBarWithCR() { - let input = "Progress: 50%\rProgress: 100%" - let actions = parse(input) - #expect(actions.count == 3) - if case .text(let s) = actions[0] { #expect(s.text == "Progress: 50%") } - guard case .carriageReturn = actions[1] else { - Issue.record("Expected .carriageReturn") - return - } - if case .text(let s) = actions[2] { #expect(s.text == "Progress: 100%") } - } } diff --git a/Tests/DevtailKitTests/PersistenceTests.swift b/Tests/DevtailKitTests/PersistenceTests.swift index a0401a4..cf9e67a 100644 --- a/Tests/DevtailKitTests/PersistenceTests.swift +++ b/Tests/DevtailKitTests/PersistenceTests.swift @@ -3,6 +3,12 @@ import Testing struct PersistenceTests { + private struct SavedAuxCommand: Codable, Equatable { + let id: UUID + var name: String + var command: String + } + private struct SavedProcess: Codable, Equatable { let id: UUID var name: String @@ -10,12 +16,6 @@ struct PersistenceTests { var workingDirectory: String var auxiliaryCommands: [SavedAuxCommand] var wasRunning: Bool - - struct SavedAuxCommand: Codable, Equatable { - let id: UUID - var name: String - var command: String - } } private let encoder = JSONEncoder() @@ -36,7 +36,7 @@ struct PersistenceTests { command: "npm run dev", workingDirectory: "/Users/test/project", auxiliaryCommands: [ - SavedProcess.SavedAuxCommand(id: auxID, name: "Tailwind", command: "npx tailwindcss --watch") + SavedAuxCommand(id: auxID, name: "Tailwind", command: "npx tailwindcss --watch") ], wasRunning: true ) @@ -67,9 +67,9 @@ struct PersistenceTests { command: "next dev", workingDirectory: "~/projects/app", auxiliaryCommands: [ - SavedProcess.SavedAuxCommand(id: UUID(), name: "CSS", command: "tailwind --watch"), - SavedProcess.SavedAuxCommand(id: UUID(), name: "TypeCheck", command: "tsc --watch"), - SavedProcess.SavedAuxCommand(id: UUID(), name: "Lint", command: "eslint --watch"), + SavedAuxCommand(id: UUID(), name: "CSS", command: "tailwind --watch"), + SavedAuxCommand(id: UUID(), name: "TypeCheck", command: "tsc --watch"), + SavedAuxCommand(id: UUID(), name: "Lint", command: "eslint --watch"), ], wasRunning: false ) @@ -152,22 +152,24 @@ struct PersistenceTests { } @Test func malformedJSONReturnsEmptyArray() { - let badData = "not valid json".data(using: .utf8)! + let badData = Data("not valid json".utf8) let result = try? JSONDecoder().decode([SavedProcess].self, from: badData) #expect(result == nil) } @Test func missingFieldsFailToDecode() { let json = """ - [{"id":"12345678-1234-1234-1234-123456789ABC","name":"Test","workingDirectory":"","auxiliaryCommands":[],"wasRunning":false}] + [{"id":"12345678-1234-1234-1234-123456789ABC",\ + "name":"Test","workingDirectory":"",\ + "auxiliaryCommands":[],"wasRunning":false}] """ - let data = json.data(using: .utf8)! + let data = Data(json.utf8) let result = try? JSONDecoder().decode([SavedProcess].self, from: data) #expect(result == nil) } @Test func emptyJSONArrayDecodesToEmpty() throws { - let data = "[]".data(using: .utf8)! + let data = Data("[]".utf8) let result = try JSONDecoder().decode([SavedProcess].self, from: data) #expect(result.isEmpty) } @@ -193,7 +195,7 @@ struct PersistenceTests { command: "echo 'Hola Mundo'", workingDirectory: "", auxiliaryCommands: [ - SavedProcess.SavedAuxCommand(id: UUID(), name: "Worker", command: "rake jobs:work") + SavedAuxCommand(id: UUID(), name: "Worker", command: "rake jobs:work") ], wasRunning: false ) @@ -222,7 +224,7 @@ struct PersistenceTests { command: "cmd \(i)", workingDirectory: "/path/\(i)", auxiliaryCommands: [ - SavedProcess.SavedAuxCommand(id: UUID(), name: "Aux \(i)", command: "aux \(i)") + SavedAuxCommand(id: UUID(), name: "Aux \(i)", command: "aux \(i)") ], wasRunning: false ) diff --git a/skills-lock.json b/skills-lock.json deleted file mode 100644 index 0281903..0000000 --- a/skills-lock.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 1, - "skills": { - "swiftui-pro": { - "source": "twostraws/swiftui-agent-skill", - "sourceType": "github", - "computedHash": "ce653d19fd00a4874489003025e9235be542e7134ff3c543e20afeb53fc3bc9a" - } - } -}