diff --git a/FrameworkTests/CloudSyncSafetyTests.swift b/FrameworkTests/CloudSyncSafetyTests.swift index 59ef2eae..c9dcfd66 100644 --- a/FrameworkTests/CloudSyncSafetyTests.swift +++ b/FrameworkTests/CloudSyncSafetyTests.swift @@ -33,30 +33,26 @@ struct CloudSyncSafetyTests { } } - @Test(arguments: SyncSafetyCategory.allCases) - func olderFetchedRecordCannotAcknowledgeTheCurrentLocalWinner(category: SyncSafetyCategory) async throws { - let fixture = try CloudSyncSafetyFixture(category: category) - defer { fixture.stop() } - try await fixture.start() - let id = fixture.recordIDs[0] - - try fixture.receive([try fixture.olderRecord(id)], []) - - #expect(try fixture.pendingNames().contains(id.recordName)) - #expect(fixture.network.nextEngine.queued.contains(id.recordName)) - #expect(fixture.record(id) != nil) + enum StaleFetchedRecord: CaseIterable { + case older + case sameVersionDifferentPayload } - @Test(arguments: SyncSafetyCategory.allCases) - func sameVersionDifferentPayloadCannotAcknowledgeTheCurrentLocalWinner( - category: SyncSafetyCategory + @Test(arguments: SyncSafetyCategory.allCases, StaleFetchedRecord.allCases) + func staleFetchedRecordCannotAcknowledgeTheCurrentLocalWinner( + category: SyncSafetyCategory, stale: StaleFetchedRecord ) async throws { let fixture = try CloudSyncSafetyFixture(category: category) defer { fixture.stop() } try await fixture.start() let id = fixture.recordIDs[0] + let fetched = + switch stale { + case .older: try fixture.olderRecord(id) + case .sameVersionDifferentPayload: try fixture.sameVersionLosingRecord(id) + } - try fixture.receive([try fixture.sameVersionLosingRecord(id)], []) + try fixture.receive([fetched], []) #expect(try fixture.pendingNames().contains(id.recordName)) #expect(fixture.network.nextEngine.queued.contains(id.recordName)) diff --git a/TFMTests/AgentPersistenceTests.swift b/TFMTests/AgentPersistenceTests.swift index c90a9e45..31d305ad 100644 --- a/TFMTests/AgentPersistenceTests.swift +++ b/TFMTests/AgentPersistenceTests.swift @@ -144,59 +144,27 @@ struct AgentPersistenceTests { @Test func schemaV2MigratesToV3WithEmptyAttachmentBacking() throws { - let directory = FileManager.default.temporaryDirectory - .appendingPathComponent("TermindAgentV3Migration-\(UUID().uuidString)") - try FileManager.default.createDirectory( - at: directory, withIntermediateDirectories: true - ) - defer { try? FileManager.default.removeItem(at: directory) } - let storeURL = directory.appendingPathComponent("Termind.store") - let legacySchema = Schema(versionedSchema: TermindSchemaV2.self) - let conversationID = UUID() - - var legacyContainer: ModelContainer? = try ModelContainer( - for: legacySchema, - configurations: [ - localModelConfiguration("Migration", schema: legacySchema, url: storeURL) - ] - ) - legacyContainer?.mainContext.insert( - PersistentAgentConversation( - id: conversationID, title: "Existing" - )) - try legacyContainer?.mainContext.save() - legacyContainer = nil - - let migrated = try ModelContainer( - for: HostPersistence.schema, - migrationPlan: TermindMigrationPlan.self, - configurations: [ - localModelConfiguration( - "Migration", schema: HostPersistence.schema, url: storeURL - ) - ] - ) - - #expect( - try migrated.mainContext.fetch( - FetchDescriptor() - ).map(\.id) == [conversationID]) - #expect( - try migrated.mainContext.fetch( - FetchDescriptor() - ).isEmpty) + try expectMigrationKeepsConversationAndLeavesEmptyBacking( + from: TermindSchemaV2.self, backing: PersistentAgentContextAttachments.self) } @Test func schemaV8MigratesToV9WithEmptyCompactionBacking() throws { + try expectMigrationKeepsConversationAndLeavesEmptyBacking( + from: TermindSchemaV8.self, backing: PersistentAgentCompaction.self) + } + + private func expectMigrationKeepsConversationAndLeavesEmptyBacking( + from legacy: any VersionedSchema.Type, backing: Backing.Type + ) throws { let directory = FileManager.default.temporaryDirectory - .appendingPathComponent("TermindAgentV9Migration-\(UUID().uuidString)") + .appendingPathComponent("TermindAgentMigration-\(UUID().uuidString)") try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true ) defer { try? FileManager.default.removeItem(at: directory) } let storeURL = directory.appendingPathComponent("Termind.store") - let legacySchema = Schema(versionedSchema: TermindSchemaV8.self) + let legacySchema = Schema(versionedSchema: legacy) let conversationID = UUID() var legacyContainer: ModelContainer? = try ModelContainer( @@ -226,10 +194,7 @@ struct AgentPersistenceTests { try migrated.mainContext.fetch( FetchDescriptor() ).map(\.id) == [conversationID]) - #expect( - try migrated.mainContext.fetch( - FetchDescriptor() - ).isEmpty) + #expect(try migrated.mainContext.fetch(FetchDescriptor()).isEmpty) } @Test diff --git a/TFMTests/AgentRuntimeTests.swift b/TFMTests/AgentRuntimeTests.swift index 3e2ecd63..ad13392e 100644 --- a/TFMTests/AgentRuntimeTests.swift +++ b/TFMTests/AgentRuntimeTests.swift @@ -521,50 +521,11 @@ struct AgentRuntimeTests { #expect(await executions.value == 0) } - @Test - func emptyCompatibleToolArgumentsBecomeAnEmptyObject() async { + @Test(arguments: ["", "null{}provider-duplicate"]) + func zeroParameterToolReceivesAnEmptyObjectForCompatibleArguments(arguments: String) async { let model = ScriptedAgentModel(turns: [ [ - .toolCallDelta(id: "1", name: "list_hosts", arguments: ""), - .finished(.toolCalls), - ], - [.textDelta("done"), .finished(.completed)], - ]) - let executions = Counter() - let tool = AnyAgentTool( - descriptor: AgentToolDescriptor( - name: "list_hosts", summary: "list", - inputSchema: .object([ - "type": .string("object"), "properties": .object([:]), - "required": .array([]), "additionalProperties": .bool(false), - ]), - target: .host, approvalPolicy: .approve - ) - ) { invocation, _ in - #expect(invocation.call.arguments == .object([:])) - await executions.increment() - return AgentToolResult(callID: invocation.call.id, content: "[]") - } - let runtime = AgentRuntime( - model: model, tools: [tool], - approval: AgentApprovalBroker(reviewer: nil, manualApproval: { _ in .allow }) - ) - for await _ in await runtime.start( - AgentRunRequest( - conversationID: UUID(), prompt: "list", permissionMode: .askForApproval - )) - {} - #expect(await executions.value == 1) - } - - @Test - func zeroParameterToolIgnoresMalformedCompatibleArguments() async { - let model = ScriptedAgentModel(turns: [ - [ - .toolCallDelta( - id: "1", name: "list_hosts", - arguments: "null{}provider-duplicate" - ), + .toolCallDelta(id: "1", name: "list_hosts", arguments: arguments), .finished(.toolCalls), ], [.textDelta("done"), .finished(.completed)], diff --git a/TFMTests/AuthorizedKeyExportTests.swift b/TFMTests/AuthorizedKeyExportTests.swift index c98e50c8..2a1a98da 100644 --- a/TFMTests/AuthorizedKeyExportTests.swift +++ b/TFMTests/AuthorizedKeyExportTests.swift @@ -7,30 +7,22 @@ import Testing @testable import TermindRemoteInfrastructure struct AuthorizedKeyExportTests { - @Test - func exportPlacesTheKeyNameCommentAboveThePublicKey() throws { - let input = try SSHConnectionOpener.authorizedKeyInstallInput( - publicKey: "ssh-ed25519 AAAAC3Nza test@example.com", - keyName: "Production" - ) + @Test(arguments: [ + ( + "ssh-ed25519 AAAAC3Nza test@example.com", "Production", + "# Production Key by Termind\nssh-ed25519 AAAAC3Nza test@example.com\n" + ), + ( + "ssh-ed25519 AAAAC3Nza", " Production\n\tServer ", + "# Production Server Key by Termind\nssh-ed25519 AAAAC3Nza\n" + ), + ]) + func exportPlacesTheKeyNameCommentAboveThePublicKeyOnOneLine( + publicKey: String, keyName: String, expected: String + ) throws { + let input = try SSHConnectionOpener.authorizedKeyInstallInput(publicKey: publicKey, keyName: keyName) - #expect( - String(decoding: input, as: UTF8.self) - == "# Production Key by Termind\nssh-ed25519 AAAAC3Nza test@example.com\n" - ) - } - - @Test - func exportKeepsTheGeneratedCommentOnOneLine() throws { - let input = try SSHConnectionOpener.authorizedKeyInstallInput( - publicKey: "ssh-ed25519 AAAAC3Nza", - keyName: " Production\n\tServer " - ) - - #expect( - String(decoding: input, as: UTF8.self) - == "# Production Server Key by Termind\nssh-ed25519 AAAAC3Nza\n" - ) + #expect(String(decoding: input, as: UTF8.self) == expected) } @Test diff --git a/TFMTests/BroadcastInputTests.swift b/TFMTests/BroadcastInputTests.swift index 6c5b7067..3a73fadc 100644 --- a/TFMTests/BroadcastInputTests.swift +++ b/TFMTests/BroadcastInputTests.swift @@ -600,11 +600,11 @@ struct BroadcastInputTests { } } - @Test - func commandQFallsThroughToAppKitQuitCommand() throws { + @Test(arguments: [("q", UInt16(12)), ("i", UInt16(34))]) + func commandShortcutFallsThroughToAppKit(character: String, keyCode: UInt16) throws { let host = Host(name: "host", host: "host.example") let sshSession = Workspace.sshSession(host: host).focusedPane.sshSession! - AppShortcutRegistry.shared.register(AppShortcutRegistration("q")) + AppShortcutRegistry.shared.register(AppShortcutRegistration(.character(Character(character)))) let event = try #require( NSEvent.keyEvent( with: .keyDown, @@ -613,32 +613,10 @@ struct BroadcastInputTests { timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: 0, context: nil, - characters: "q", - charactersIgnoringModifiers: "q", + characters: character, + charactersIgnoringModifiers: character, isARepeat: false, - keyCode: 12 - )) - - #expect(!sshSession.terminalViewOwner.performKeyEquivalentForTesting(event)) - } - - @Test - func commandIFallsThroughToAppKitGenerateCommand() throws { - let host = Host(name: "host", host: "host.example") - let sshSession = Workspace.sshSession(host: host).focusedPane.sshSession! - AppShortcutRegistry.shared.register(AppShortcutRegistration("i")) - let event = try #require( - NSEvent.keyEvent( - with: .keyDown, - location: .zero, - modifierFlags: .command, - timestamp: ProcessInfo.processInfo.systemUptime, - windowNumber: 0, - context: nil, - characters: "i", - charactersIgnoringModifiers: "i", - isARepeat: false, - keyCode: 34 + keyCode: keyCode )) #expect(!sshSession.terminalViewOwner.performKeyEquivalentForTesting(event)) diff --git a/TFMTests/ExternalRequestTests.swift b/TFMTests/ExternalRequestTests.swift index 9c1dc9fe..b6a5e1db 100644 --- a/TFMTests/ExternalRequestTests.swift +++ b/TFMTests/ExternalRequestTests.swift @@ -395,30 +395,8 @@ struct ExternalAuthCoordinatorTests { @Suite(.serialized) @MainActor struct ExternalToolCallHistoryTests { - @Test - func clearingHistoryInvalidatesObservedEntriesImmediately() { - let entry = ExternalToolCall( - runID: UUID(), clientName: "Codex", toolName: "list_hosts", - summary: "List saved hosts", risk: .readOnly, - startedAt: Date(timeIntervalSince1970: 1) - ) - let store = ToolAuditModel(entries: [entry]) - let invalidated = ObservationInvalidationFlag() - - withObservationTracking { - #expect(store.entries().map(\.id) == [entry.id]) - } onChange: { - invalidated.value = true - } - - store.clear() - - #expect(invalidated.value) - #expect(store.entries().isEmpty) - } - - @Test - func clearingHistoryInvalidatesObservedFilteredEntriesImmediately() { + @Test(arguments: [nil, "uname"]) + func clearingHistoryInvalidatesObservedEntriesImmediately(filter: String?) { let entry = ExternalToolCall( runID: UUID(), clientName: "Claude Code", toolName: "ssh_exec", summary: "uname -a", risk: .readOnly, @@ -426,9 +404,12 @@ struct ExternalToolCallHistoryTests { ) let store = ToolAuditModel(entries: [entry]) let invalidated = ObservationInvalidationFlag() + func observedEntries() -> [ExternalToolCall] { + filter.map { store.entries(matching: $0) } ?? store.entries() + } withObservationTracking { - #expect(store.entries(matching: "uname").map(\.id) == [entry.id]) + #expect(observedEntries().map(\.id) == [entry.id]) } onChange: { invalidated.value = true } @@ -436,7 +417,7 @@ struct ExternalToolCallHistoryTests { store.clear() #expect(invalidated.value) - #expect(store.entries(matching: "uname").isEmpty) + #expect(observedEntries().isEmpty) } @Test diff --git a/TFMTests/MarkdownParsingTests.swift b/TFMTests/MarkdownParsingTests.swift index 8050b0bf..a9004c8d 100644 --- a/TFMTests/MarkdownParsingTests.swift +++ b/TFMTests/MarkdownParsingTests.swift @@ -72,26 +72,18 @@ struct MarkdownParsingTests { #expect(paragraphText(items[1].children.first) == "todo") } - @Test - func fencedCodeKeepsItsLanguageAndNewlines() { - let blocks = TermindMarkdownParser.parse("```bash\nls -la\ncd /tmp\n```") + @Test(arguments: [ + ("```bash\nls -la\ncd /tmp\n```", "ls -la\ncd /tmp"), + ("```bash\nls -la", "ls -la"), + ]) + func fencedCodeKeepsItsLanguageAndNewlines(markdown: String, expectedCode: String) { + let blocks = TermindMarkdownParser.parse(markdown) guard case .codeBlock(let language, let code)? = blocks.first else { Issue.record("expected a code block, got \(blocks)") return } #expect(language == "bash") - #expect(code.trimmingCharacters(in: .newlines) == "ls -la\ncd /tmp") - } - - @Test - func unterminatedFenceStillProducesACodeBlock() { - let blocks = TermindMarkdownParser.parse("```bash\nls -la") - guard case .codeBlock(let language, let code)? = blocks.first else { - Issue.record("expected a code block, got \(blocks)") - return - } - #expect(language == "bash") - #expect(code.trimmingCharacters(in: .newlines) == "ls -la") + #expect(code.trimmingCharacters(in: .newlines) == expectedCode) } @Test(arguments: ["https://example.com", "file:///tmp/example.txt", "ssh://example.com", "custom:document"])