@@ -391,4 +391,74 @@ final class LiveTailViewModelTests: XCTestCase {
391391 vm.moveColumn(from: IndexSet(integer: 0), to: 3)
392392 XCTAssertEqual(vm.columnOrder, ["b", "c", "a"])
393393 }
394+
395+ // MARK: - Column Filters
396+
397+ func testAddColumnFilter() {
398+ let vm = LiveTailViewModel()
399+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
400+ XCTAssertEqual(vm.columnFilters.count, 1)
401+ XCTAssertEqual(vm.columnFilters[0].column, "level")
402+ XCTAssertEqual(vm.columnFilters[0].value, .string("error"))
403+ XCTAssertFalse(vm.columnFilters[0].exclude)
404+ }
405+
406+ func testAddColumnFilterExclude() {
407+ let vm = LiveTailViewModel()
408+ vm.addColumnFilter(column: "level", value: .string("debug"), exclude: true)
409+ XCTAssertEqual(vm.columnFilters.count, 1)
410+ XCTAssertTrue(vm.columnFilters[0].exclude)
411+ }
412+
413+ func testAddDuplicateFilterReplacesExisting() {
414+ let vm = LiveTailViewModel()
415+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
416+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
417+ XCTAssertEqual(vm.columnFilters.count, 1)
418+ }
419+
420+ func testAddFilterDifferentValueIsAdditive() {
421+ let vm = LiveTailViewModel()
422+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
423+ vm.addColumnFilter(column: "level", value: .string("warn"), exclude: false)
424+ XCTAssertEqual(vm.columnFilters.count, 2)
425+ }
426+
427+ func testRemoveColumnFilter() {
428+ let vm = LiveTailViewModel()
429+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
430+ let filter = vm.columnFilters[0]
431+ vm.removeColumnFilter(filter)
432+ XCTAssertTrue(vm.columnFilters.isEmpty)
433+ }
434+
435+ func testClearColumnFilters() {
436+ let vm = LiveTailViewModel()
437+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
438+ vm.addColumnFilter(column: "status", value: .int(500), exclude: true)
439+ vm.clearColumnFilters()
440+ XCTAssertTrue(vm.columnFilters.isEmpty)
441+ }
442+
443+ func testClearResetsColumnFilters() {
444+ let vm = LiveTailViewModel()
445+ vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
446+ vm.clear()
447+ XCTAssertTrue(vm.columnFilters.isEmpty)
448+ }
449+
450+ func testColumnFilterDisplayLabelInclude() {
451+ let filter = LiveTailViewModel.ColumnFilter(column: "level", value: .string("error"), exclude: false)
452+ XCTAssertEqual(filter.displayLabel, "level = error")
453+ }
454+
455+ func testColumnFilterDisplayLabelExclude() {
456+ let filter = LiveTailViewModel.ColumnFilter(column: "level", value: .string("debug"), exclude: true)
457+ XCTAssertEqual(filter.displayLabel, "level \u{2260} debug")
458+ }
459+
460+ func testColumnFilterDisplayLabelNull() {
461+ let filter = LiveTailViewModel.ColumnFilter(column: "field", value: nil, exclude: false)
462+ XCTAssertEqual(filter.displayLabel, "field = null")
463+ }
394464}