diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a319f20..8c47e5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,7 +128,7 @@ scripts/run-live-codex-file-scenario.sh scripts/run-live-codex-rollback-scenario.sh ``` -Reports default to `tmp/live-codex-reports/`. Use `SWIFTASB_LIVE_CODEX_REPORT_DIR` for another report directory, `SWIFTASB_LIVE_CODEX_BIN` for a specific Codex executable, and `SWIFTASB_LIVE_CODEX_KEEP_WORKSPACES=1` to preserve temporary workspaces for debugging. +Reports default to `tmp/live-codex-reports/`. Use `SWIFTASB_LIVE_CODEX_REPORT_DIR` for another report directory, `SWIFTASB_LIVE_CODEX_BIN` for a specific Codex executable, `SWIFTASB_LIVE_CODEX_TIMEOUT_SECONDS` to override per-operation live probe timeouts, and `SWIFTASB_LIVE_CODEX_KEEP_WORKSPACES=1` to preserve temporary workspaces for debugging. ### Maintainer Scripts diff --git a/README.md b/README.md index 86654c3..a46ce5a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Use SwiftASB when an app needs to show what Codex is doing right now, keep recen For app-wide sidebars and launchers, `CodexAppServer.makeLibrary()` provides observable stored-thread lists, cwd or repository grouping, refresh actions, library-local selection state, and app-wide model, MCP, and hook diagnostics snapshots. Thread handles can also name, archive, unarchive, compact, and roll back stored threads through thread-scoped methods. -Use `CodexAppServer.fs` when a sandboxed client needs filesystem metadata, directory listings, file bytes, file discovery, fuzzy file lookup, or file-change watches through the Codex app-server instead of reading local disk directly. `CodexWorkspace` carries app-server-owned workspace permission selections, active permission-profile provenance, and runtime filesystem/network permission facts for started threads and turns. Use `CodexAppServer.config` for effective config reads, and `CodexAppServer.extensions` for app, skill, plugin, and collaboration-mode inventory. +Use `CodexAppServer.fs` when a sandboxed client needs filesystem metadata, directory listings, file bytes, file discovery, fuzzy file lookup, or file-change watches through the Codex app-server instead of reading local disk directly. File-discovery hits include match kind, matched character ranges, and ranking reasons for picker highlighting and result explanations. `CodexWorkspace` carries app-server-owned workspace permission selections, active permission-profile provenance, and runtime filesystem/network permission facts for started threads and turns. Use `CodexAppServer.config` for effective config reads, and `CodexAppServer.extensions` for app, skill, plugin, and collaboration-mode inventory. Use `CodexAppServer.ThreadListQD`, `CodexFS.FileDiscoveryQD`, `CodexThread.HistoryWindowQD`, `CodexThread.RecentFilesQD`, and `CodexThread.RecentCommandsQD` when a client needs to preserve repeatable list, file-discovery, history-window, or recent-activity intent without depending on Core Data, SwiftData, direct filesystem reads, or raw app-server paging details. diff --git a/ROADMAP.md b/ROADMAP.md index 7a822f3..0522c41 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -57,7 +57,7 @@ | Stored thread resume flow | `Shipped` | `resumeThread(...)` wraps `thread/resume`, returns a normal `CodexThread`, restores thread defaults, clears stale archived state for the reopened thread, and hydrates any resumed persisted turns into the same local history store without resetting completeness to a fresh-thread state. Callers can set `excludeTurns` when they plan to page history separately through `thread/turns/list`. | | Stored thread fork flow | `Shipped` | `forkThread(...)` wraps `thread/fork`, returns a normal `CodexThread`, persists copied fork history into thread-scoped local turn rows, and records explicit fork lineage through the source thread id plus the last shared turn id. Callers can set `excludeTurns` when they want the fork metadata first and copied turn history through paged reads afterward. | | Thread management actions | `Partially shipped` | `CodexThread.setName(...)` wraps `thread/name/set`, `CodexThread.archive()` wraps `thread/archive`, `CodexThread.unarchive()` wraps `thread/unarchive`, `CodexThread.updateMetadata(...)` wraps `thread/metadata/update`, and `CodexThread.rollbackLastTurns(...)` wraps `thread/rollback`. Metadata patches use an explicit replace/clear/unchanged field model so callers can express upstream null-vs-omitted semantics. Rollback reconciles visible local history to the app-server response, records a rollback marker, and now has opt-in live coverage against a disposable non-ephemeral thread, but it does not preserve full removed turn payloads as forensic archive data yet. | -| App-server filesystem reads and watches | `Partially shipped` | `CodexAppServer.fs` now exposes the `CodexFS` namespace for app-server-routed metadata, directory listing, file-byte reads, bounded file discovery, SwiftASB-owned fuzzy ranking over app-server-returned entries, and filesystem watch notifications. This gives sandboxed clients a Codex-owned path for basic filesystem facts and picker/search views instead of requiring direct local disk reads. File mutations and repository-root discovery remain separate schema families for later promotion decisions. | +| App-server filesystem reads and watches | `Partially shipped` | `CodexAppServer.fs` now exposes the `CodexFS` namespace for app-server-routed metadata, directory listing, file-byte reads, bounded file discovery, SwiftASB-owned fuzzy ranking over app-server-returned entries, UI-ready discovery match metadata, and filesystem watch notifications. This gives sandboxed clients a Codex-owned path for basic filesystem facts and picker/search views instead of requiring direct local disk reads. File mutations and repository-root discovery remain separate schema families for later promotion decisions. | | App-server config reads | `Partially shipped` | `CodexAppServer.config` now exposes `CodexConfig` for effective config and requirements reads through the app-server. Effective config stays JSON-shaped for now so SwiftASB does not turn unstable config keys into long-lived public Swift fields too early. | | App-server extension inventory | `Partially shipped` | `CodexAppServer.extensions` now exposes `CodexAppServer.CodexExtensions` for app, skill, plugin, and collaboration-mode inventory. Plugin install/uninstall/upgrade and skills config writes remain unpromoted until their permission and review model is clearer. | | Thread goals | `Partially shipped` | `CodexThread.readGoal()`, `setGoal(...)`, and `clearGoal()` wrap `thread/goal/get`, `thread/goal/set`, and `thread/goal/clear`, and thread event streams now surface goal updated and cleared notifications. | @@ -843,14 +843,16 @@ The live script surface should support these environment knobs consistently: ### First Implementation Slice The first post-v1 live-testing slice is the consolidated release-gate runner. -It runs the currently proven high-signal probes in order: deterministic -approval/server-request, multi-turn file mutation, and rollback. The permissions +It runs the currently proven high-signal probes in order: broad smoke coverage +for startup, raw transport initialize/thread/turn, binary diagnostics, +app-wide model/MCP/hook snapshots, thread-name mutation, single-turn, +cross-thread, and same-thread behavior; deterministic approval/server-request +coverage; the multi-turn file mutation scenario; and rollback. The permissions approval mock-Responses probe now covers the largest answerable server-request family gap. The umbrella live integration-test runner now gives maintainers one -entrypoint for release-gate, focused, and full opt-in live coverage. The next -slice should either add focused modes around remaining promoted request -families or broaden the release gate with startup, initialize, model/MCP, -single-turn, and cross-thread probes if their runtime cost stays reasonable. +entrypoint for release-gate, focused, and full opt-in live coverage, with +`SWIFTASB_LIVE_CODEX_TIMEOUT_SECONDS` available when a slower runtime needs a +longer per-operation timeout. ## Previous V1 Release Slice @@ -1221,7 +1223,7 @@ Completed - [ ] Add marketplace upgrade and account-management surfaces after SwiftASB has a concrete app-wide management workflow. - [ ] Add external-agent config import surfaces after external-agent configuration becomes a public app-server management workflow. - [ ] Add structured patch rendering for `RecentFiles`. -- [ ] Add richer `CodexFS.FileDiscoveryHit` search metadata soon, including +- [x] Add richer `CodexFS.FileDiscoveryHit` search metadata soon, including match kind, matched ranges, or ranking reason once UI highlighting needs an explicit public model. - [ ] Promote an upstream app-server fuzzy file-search endpoint later if Codex @@ -1246,3 +1248,4 @@ Completed - 2026-05-06: Promoted workspace permission-profile selections and runtime permission facts through `CodexWorkspace`, and exposed active permission profiles on thread sessions and handles. - 2026-05-06: Promoted bounded file discovery and fuzzy file lookup through `CodexFS.FileDiscoveryQD` and `CodexFS.discoverFiles(_:)`, keeping traversal on app-server `fs/readDirectory` while SwiftASB owns local ranking over returned entries. - 2026-05-06: Expanded deterministic coverage for promoted file discovery, config, extension inventory, and workspace-permission request descriptors. +- 2026-05-07: Added UI-ready `CodexFS.FileDiscoveryHit` search metadata for match kind, matched file-name and relative-path character ranges, and stable ranking reasons. diff --git a/Sources/SwiftASB/Public/CodexFS.swift b/Sources/SwiftASB/Public/CodexFS.swift index 10c980b..f834506 100644 --- a/Sources/SwiftASB/Public/CodexFS.swift +++ b/Sources/SwiftASB/Public/CodexFS.swift @@ -248,12 +248,74 @@ public struct CodexFS: Sendable { case other } + /// Search match shape that best explains why a hit ranked. + public enum MatchKind: String, Sendable, Equatable { + /// The normalized file name exactly equals the normalized search term. + case exactFileName + /// The normalized file name starts with the normalized search term. + case fileNamePrefix + /// The normalized file name contains the normalized search term. + case fileNameContains + /// The normalized relative path contains the normalized search term. + case relativePathContains + /// Word initials in the file name match the normalized search term. + case acronym + /// Search characters matched in order without a stronger contiguous match. + case subsequence + } + + /// Character-offset range for highlighting matched text. + public struct MatchRange: Sendable, Equatable { + /// Number of matched characters in this contiguous range. + public let length: Int + /// Zero-based character offset where the match range begins. + public let start: Int + } + + /// Ranking signal that contributed to a fuzzy file-discovery score. + public struct RankingReason: Sendable, Equatable { + /// Stable reason category for UI explanations. + public enum Kind: String, Sendable, Equatable { + /// File-name initials matched the search term. + case acronymMatch + /// File name exactly matched the search term. + case exactFileName + /// File name contained the search term. + case fileNameContains + /// File name started with the search term. + case fileNamePrefix + /// File name matched the search term as an ordered subsequence. + case fileNameSubsequence + /// Generated or build-output path components lowered the score. + case generatedPathPenalty + /// A matched character landed at a path, word, or punctuation boundary. + case pathBoundaryMatch + /// Relative path contained the search term. + case relativePathContains + /// Relative path matched the search term as an ordered subsequence. + case relativePathSubsequence + } + + /// Stable reason category for the ranking signal. + public let kind: Kind + /// Score contribution for the signal. Penalties use negative values. + public let value: Int + } + public var id: String { path } public let depth: Int public let fileName: String public let kind: Kind + /// Strongest search match shape for this hit, or nil when no search term was used. + public let matchKind: MatchKind? + /// Character ranges in `fileName` that matched the search term. + public let matchedFileNameRanges: [MatchRange] + /// Character ranges in `relativePath` that matched the search term. + public let matchedRelativePathRanges: [MatchRange] public let path: String + /// Stable ranking signals that explain the fuzzy score. + public let rankingReasons: [RankingReason] public let relativePath: String public let score: Int? } @@ -384,21 +446,25 @@ private extension CodexFS { let childPath = appendingPathComponent(entry.fileName, to: directoryPath) let relativePath = appendingPathComponent(entry.fileName, to: relativeDirectoryPath) let kind = CodexFS.FileDiscoveryHit.Kind(entry.kind) - let score = query.searchTerm.flatMap { - fuzzyScore(query: $0, candidate: relativePath) + let match = query.searchTerm.flatMap { + fuzzyMatch(query: $0, relativePath: relativePath) } if query.includedKinds.contains(kind), - query.searchTerm == nil || score != nil + query.searchTerm == nil || match != nil { hits.append( .init( depth: depth, fileName: entry.fileName, kind: kind, + matchKind: match?.kind, + matchedFileNameRanges: match?.fileNameRanges ?? [], + matchedRelativePathRanges: match?.relativePathRanges ?? [], path: childPath, + rankingReasons: match?.rankingReasons ?? [], relativePath: relativePath, - score: score + score: match?.score ) ) } @@ -422,44 +488,86 @@ private extension CodexFS { return path.hasSuffix("/") ? path + component : path + "/" + component } - func fuzzyScore(query: String, candidate: String) -> Int? { + func fuzzyMatch(query: String, relativePath: String) -> FileDiscoveryMatch? { let normalizedQuery = query.lowercased() let queryCharacters = Array(normalizedQuery) guard !queryCharacters.isEmpty else { return nil } - let normalizedCandidate = candidate.lowercased() - let baseName = URL(fileURLWithPath: candidate).lastPathComponent.lowercased() + let normalizedRelativePath = relativePath.lowercased() + let fileName = URL(fileURLWithPath: relativePath).lastPathComponent + let normalizedFileName = fileName.lowercased() - guard let pathScore = subsequenceScore(queryCharacters: queryCharacters, candidate: normalizedCandidate) else { + guard let pathMatch = subsequenceScore(queryCharacters: queryCharacters, candidate: normalizedRelativePath) else { return nil } - var score = pathScore - if let baseNameScore = subsequenceScore(queryCharacters: queryCharacters, candidate: baseName) { - score = max(score, baseNameScore + 35) + var score = pathMatch.score + var kind = CodexFS.FileDiscoveryHit.MatchKind.subsequence + var fileNameRanges: [CodexFS.FileDiscoveryHit.MatchRange] = [] + var reasons: [CodexFS.FileDiscoveryHit.RankingReason] = [ + .init(kind: .relativePathSubsequence, value: pathMatch.score), + ] + + if pathMatch.hasBoundaryMatch { + reasons.append(.init(kind: .pathBoundaryMatch, value: 8)) + } + + if let fileNameMatch = subsequenceScore(queryCharacters: queryCharacters, candidate: normalizedFileName) { + let fileNameScore = fileNameMatch.score + 35 + score = max(score, fileNameScore) + fileNameRanges = fileNameMatch.ranges + reasons.append(.init(kind: .fileNameSubsequence, value: fileNameScore)) + if fileNameMatch.hasBoundaryMatch { + reasons.append(.init(kind: .pathBoundaryMatch, value: 8)) + } } - if baseName == normalizedQuery { + if normalizedFileName == normalizedQuery { score += 120 - } else if baseName.hasPrefix(normalizedQuery) { + kind = .exactFileName + reasons.append(.init(kind: .exactFileName, value: 120)) + } else if normalizedFileName.hasPrefix(normalizedQuery) { score += 80 - } else if baseName.contains(normalizedQuery) { + kind = .fileNamePrefix + reasons.append(.init(kind: .fileNamePrefix, value: 80)) + } else if normalizedFileName.contains(normalizedQuery) { score += 60 - } else if normalizedCandidate.contains(normalizedQuery) { + kind = .fileNameContains + reasons.append(.init(kind: .fileNameContains, value: 60)) + } else if normalizedRelativePath.contains(normalizedQuery) { score += 25 + kind = .relativePathContains + reasons.append(.init(kind: .relativePathContains, value: 25)) } - if acronymMatches(query: normalizedQuery, candidate: baseName) { + if acronymMatches(query: normalizedQuery, candidate: normalizedFileName) { score += 35 + if kind == .subsequence { + kind = .acronym + } + reasons.append(.init(kind: .acronymMatch, value: 35)) + } + + let penalty = generatedPathPenalty(candidate: normalizedRelativePath) + if penalty > 0 { + reasons.append(.init(kind: .generatedPathPenalty, value: -penalty)) } - return score - generatedPathPenalty(candidate: normalizedCandidate) + return .init( + fileNameRanges: fileNameRanges, + kind: kind, + rankingReasons: reasons, + relativePathRanges: pathMatch.ranges, + score: score - penalty + ) } - func subsequenceScore(queryCharacters: [Character], candidate: String) -> Int? { + func subsequenceScore(queryCharacters: [Character], candidate: String) -> SubsequenceMatch? { let candidateCharacters = Array(candidate) var queryIndex = 0 var score = 0 + var matchedOffsets: [Int] = [] + var hasBoundaryMatch = false var previousMatchIndex: Int? for (candidateIndex, candidateCharacter) in candidateCharacters.enumerated() { @@ -468,22 +576,50 @@ private extension CodexFS { score += 10 if candidateIndex == 0 || isPathBoundary(candidateCharacters[candidateIndex - 1]) { score += 8 + hasBoundaryMatch = true } if let previousMatchIndex { score += max(0, 6 - (candidateIndex - previousMatchIndex - 1)) } + matchedOffsets.append(candidateIndex) previousMatchIndex = candidateIndex queryIndex += 1 if queryIndex == queryCharacters.count { - return score - candidateCharacters.count + return .init( + hasBoundaryMatch: hasBoundaryMatch, + ranges: matchRanges(from: matchedOffsets), + score: score - candidateCharacters.count + ) } } return nil } + func matchRanges(from offsets: [Int]) -> [CodexFS.FileDiscoveryHit.MatchRange] { + guard let firstOffset = offsets.first else { return [] } + + var ranges: [CodexFS.FileDiscoveryHit.MatchRange] = [] + var rangeStart = firstOffset + var previousOffset = firstOffset + + for offset in offsets.dropFirst() { + if offset == previousOffset + 1 { + previousOffset = offset + continue + } + + ranges.append(.init(length: previousOffset - rangeStart + 1, start: rangeStart)) + rangeStart = offset + previousOffset = offset + } + + ranges.append(.init(length: previousOffset - rangeStart + 1, start: rangeStart)) + return ranges + } + func acronymMatches(query: String, candidate: String) -> Bool { let words = candidate.split { character in isPathBoundary(character) @@ -512,6 +648,20 @@ private extension CodexFS { func isPathBoundary(_ character: Character) -> Bool { character == "/" || character == "-" || character == "_" || character == "." || character == " " } + + struct FileDiscoveryMatch { + var fileNameRanges: [FileDiscoveryHit.MatchRange] + var kind: FileDiscoveryHit.MatchKind + var rankingReasons: [FileDiscoveryHit.RankingReason] + var relativePathRanges: [FileDiscoveryHit.MatchRange] + var score: Int + } + + struct SubsequenceMatch { + var hasBoundaryMatch: Bool + var ranges: [FileDiscoveryHit.MatchRange] + var score: Int + } } private extension CodexFS.FileDiscoveryHit.Kind { diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexFS.md b/Sources/SwiftASB/SwiftASB.docc/CodexFS.md index cafdc18..9151d18 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexFS.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexFS.md @@ -20,6 +20,13 @@ let files = try await appServer.fs.discoverFiles( ) ``` +Each ``FileDiscoveryHit`` includes UI-ready search metadata when a search term +is present: ``FileDiscoveryHit/matchKind``, ``FileDiscoveryHit/matchedFileNameRanges``, +``FileDiscoveryHit/matchedRelativePathRanges``, and +``FileDiscoveryHit/rankingReasons``. Use those values to highlight matched +characters and explain why one result ranked above another without duplicating +SwiftASB's fuzzy scoring in app code. + ## Topics ### Reads diff --git a/Tests/SwiftASBTests/Public/CodexAppServerFileSystemTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerFileSystemTests.swift index b903758..80257cb 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerFileSystemTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerFileSystemTests.swift @@ -75,6 +75,17 @@ extension CodexAppServerTests { #expect(result.hits.first?.path == "/tmp/project/Sources/SwiftASB/CodexFS.swift") #expect(result.hits.first?.kind == .file) #expect(result.hits.first?.depth == 2) + #expect(result.hits.first?.matchKind == .subsequence) + #expect(result.hits.first?.matchedFileNameRanges == [ + .init(length: 1, start: 0), + .init(length: 3, start: 4), + ]) + #expect(result.hits.first?.matchedRelativePathRanges == [ + .init(length: 1, start: 4), + .init(length: 3, start: 21), + ]) + #expect(result.hits.first?.rankingReasons.map(\.kind).contains(.relativePathSubsequence) == true) + #expect(result.hits.first?.rankingReasons.map(\.kind).contains(.fileNameSubsequence) == true) #expect(result.hits.first?.score != nil) let directoryRequests = await transport.requestPayloads(for: "fs/readDirectory") @@ -135,6 +146,14 @@ extension CodexAppServerTests { ".build/debug/CodexFS.o", ]) #expect(hiddenEntries.hits.map(\.kind) == [.file, .directory, .file, .file, .file]) + #expect(hiddenEntries.hits.first?.matchKind == .subsequence) + #expect(hiddenEntries.hits.first?.matchedFileNameRanges == [ + .init(length: 1, start: 0), + .init(length: 2, start: 5), + ]) + #expect( + hiddenEntries.hits.last?.rankingReasons.contains(.init(kind: .generatedPathPenalty, value: -105)) == true + ) let noMatches = try await client.fs.discoverFiles( .files(under: "/tmp/project", matching: "definitely-not-here", limit: 10, maximumDepth: 3) diff --git a/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTestSupport.swift b/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTestSupport.swift index dbdc859..d7ed2e0 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTestSupport.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTestSupport.swift @@ -1484,7 +1484,7 @@ func runApprovalProbeCaseReport( do { let result = try await completeLiveTurnAcceptingApprovals( turn, - timeoutSeconds: 90, + timeoutSeconds: liveTimeoutSeconds(default: 90), operation: "waiting for the \(probeCase.label) approval probe to complete" ) return .init(probeCase, thread: thread, result: result) @@ -1534,7 +1534,7 @@ func runBehaviorMatrixCase( ) let result = try await completeLiveTurnAcceptingApprovals( turn, - timeoutSeconds: 90, + timeoutSeconds: liveTimeoutSeconds(default: 90), operation: "waiting for the \(matrixCase.label) behavior-matrix case to complete" ) return .init( @@ -1635,7 +1635,7 @@ func probeLiveSameThreadMatrix( case let .failed(errorDescription): let completion = try? await awaitCompletion( of: firstTurn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the first behavior-matrix same-thread turn to complete" ) return .init( @@ -2208,6 +2208,16 @@ func withTimeout( } } +func liveTimeoutSeconds(default defaultSeconds: Double) -> Double { + guard let rawValue = ProcessInfo.processInfo.environment["SWIFTASB_LIVE_CODEX_TIMEOUT_SECONDS"], + let seconds = Double(rawValue), + seconds > 0 + else { + return defaultSeconds + } + return seconds +} + func prompt(label: String) -> String { """ This is a live SwiftASB integration test. diff --git a/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTests.swift index cfcaac3..d06b6cc 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerLiveIntegrationTests.swift @@ -412,7 +412,7 @@ struct CodexAppServerLiveIntegrationTests { let completion = try await awaitCompletion( of: turn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the single live turn to complete" ) #expect(completion.turn.status == .completed) @@ -461,12 +461,12 @@ struct CodexAppServerLiveIntegrationTests { async let crossThreadCompletionA = awaitCompletion( of: crossThreadTurnA, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the first cross-thread turn to complete" ) async let crossThreadCompletionB = awaitCompletion( of: crossThreadTurnB, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the second cross-thread turn to complete" ) @@ -627,7 +627,7 @@ struct CodexAppServerLiveIntegrationTests { ) let firstCompletion = try await awaitCompletion( of: firstTurn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the first live rollback turn to complete" ) #expect(firstCompletion.turn.status == .completed) @@ -638,7 +638,7 @@ struct CodexAppServerLiveIntegrationTests { ) let secondCompletion = try await awaitCompletion( of: secondTurn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the second live rollback turn to complete" ) #expect(secondCompletion.turn.status == .completed) @@ -712,7 +712,7 @@ struct CodexAppServerLiveIntegrationTests { ) let firstCompletion = try await awaitCompletion( of: firstTurn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the first live history turn to complete" ) #expect(firstCompletion.turn.status == .completed) @@ -723,7 +723,7 @@ struct CodexAppServerLiveIntegrationTests { ) let secondCompletion = try await awaitCompletion( of: secondTurn, - timeoutSeconds: 45, + timeoutSeconds: liveTimeoutSeconds(default: 45), operation: "waiting for the second live history turn to complete" ) #expect(secondCompletion.turn.status == .completed) diff --git a/scripts/run-live-codex-integration-tests.sh b/scripts/run-live-codex-integration-tests.sh index 59d512a..fb58da7 100755 --- a/scripts/run-live-codex-integration-tests.sh +++ b/scripts/run-live-codex-integration-tests.sh @@ -25,6 +25,8 @@ Modes: help Show this help text. Environment: + SWIFTASB_LIVE_CODEX_TIMEOUT_SECONDS + Override per-operation live probe timeouts. SWIFTASB_LIVE_CODEX_REPORT_DIR Directory for JSON reports from reporting probes. SWIFTASB_LIVE_CODEX_BIN Codex executable path to test instead of PATH discovery. SWIFTASB_LIVE_CODEX_KEEP_WORKSPACES=1 diff --git a/scripts/run-live-codex-release-gate.sh b/scripts/run-live-codex-release-gate.sh index 74b04b8..9b3b8f1 100755 --- a/scripts/run-live-codex-release-gate.sh +++ b/scripts/run-live-codex-release-gate.sh @@ -10,7 +10,7 @@ export SWIFTASB_LIVE_CODEX_REPORT_DIR mkdir -p "$SWIFTASB_LIVE_CODEX_REPORT_DIR" printf '%s\n' 'Running SwiftASB live Codex release gate.' -printf '%s\n' 'Step 1/4: live smoke probes' +printf '%s\n' 'Step 1/4: startup, transport, capability, thread, turn, and concurrency smoke probes' env SWIFTASB_ENABLE_LIVE_CODEX_TRANSPORT_TESTS=1 \ SWIFTASB_ENABLE_LIVE_CODEX_CAPABILITY_TESTS=1 \ SWIFTASB_ENABLE_LIVE_CODEX_THREAD_MANAGEMENT_TESTS=1 \