Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions Rust/KeyPathKanataHostBridge/src/session_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@ fn key_supported(key: KeyCode, usages: &[u32]) -> bool {
oscode_supported(key.into(), usages)
}

/// The one output rule: keyboard page (7) and in the host's key map. Consumer
/// keys (media, brightness) and Fn are on other pages, so they are refused here.
/// Consumer-page (0x0C) usages the session worker posts as macOS
/// system-defined media events (docs/architecture/driverless-media-keys.md
/// §Recommendation): volume up/down/mute, brightness up/down, play/pause,
/// next/previous, fast-forward/rewind. Fixed by the worker's code, not by the
/// host's keyboard map, so it is a constant here rather than a parameter.
pub(crate) const CONSUMER_OUTPUT_CODES: [u32; 10] =
[0xE9, 0xEA, 0xE2, 0x6F, 0x70, 0xCD, 0xB5, 0xB6, 0xB3, 0xB4];

/// The one output rule: keyboard page (7) and in the host's key map, or one
/// of the consumer-page media keys the worker can post. Everything else (Fn,
/// other consumer usages) is refused.
fn oscode_supported(code: OsCode, usages: &[u32]) -> bool {
PageCode::try_from(code).is_ok_and(|pc| pc.page == 7 && usages.contains(&pc.code))
PageCode::try_from(code).is_ok_and(|pc| {
(pc.page == 7 && usages.contains(&pc.code))
|| (pc.page == 0x0C && CONSUMER_OUTPUT_CODES.contains(&pc.code))
})
}

/// Outputs the session can emit: the host's key map plus native Caps Lock,
Expand Down Expand Up @@ -327,18 +339,39 @@ mod tests {
assert_eq!(output_key_name_supported("home", HOST), Some(true));
// Native Caps is always an output, even though the host omits it.
assert_eq!(output_key_name_supported("caps", HOST), Some(true));
// Consumer page: media and brightness are refused.
// Consumer page: the media and brightness keys the worker posts.
for name in [
"volu", "voldwn", "mute", "pp", "next", "prev", "brup", "brdown",
] {
assert_eq!(output_key_name_supported(name, HOST), Some(false), "{name}");
assert_eq!(output_key_name_supported(name, HOST), Some(true), "{name}");
}
// Consumer page, but not one the worker posts.
assert_eq!(output_key_name_supported("calc", HOST), Some(false));
// Keyboard page but outside the host map.
assert_eq!(output_key_name_supported("f13", HOST), Some(false));
// Not a Kanata key at all.
assert_eq!(output_key_name_supported("not-a-key", HOST), None);
}

#[test]
fn consumer_output_table_matches_kanata_names_and_inputs_stay_keyboard_only() {
for (name, code) in [
("volu", 0xE9), ("vold", 0xEA), ("mute", 0xE2), ("brup", 0x6F), ("brdn", 0x70),
("pp", 0xCD), ("next", 0xB5), ("prev", 0xB6),
] {
let os = kanata_parser::keys::str_to_oscode(name).unwrap();
let pc = PageCode::try_from(os).unwrap();
assert_eq!((pc.page, pc.code), (0x0C, code), "{name}");
assert!(CONSUMER_OUTPUT_CODES.contains(&pc.code), "{name}");
}
// Until the tap reads media events, a media key is not an input.
let cfg = kanata_parser::cfg::new_from_str("(defsrc volu)(deflayer base a)", Default::default()).unwrap();
assert!(!supported(&cfg, HOST));
// ...but it is an output anywhere a keyboard key would be.
let cfg = kanata_parser::cfg::new_from_str("(defsrc a)(deflayer base volu)", Default::default()).unwrap();
assert!(supported(&cfg, HOST));
}

#[test]
fn native_caps_output_does_not_require_capturing_physical_caps() {
for action in [
Expand Down Expand Up @@ -566,7 +599,7 @@ mod tests {
"((input virtual vk_test))"
));
assert!(!eligible(
"(switch ((input virtual vk_test)) volu break () b break)",
"(switch ((input virtual vk_test)) calc break () b break)",
"((input virtual vk_test))"
));
assert!(!eligible("(unicode λ)", "((input virtual vk_test))"));
Expand All @@ -582,10 +615,10 @@ mod tests {
"(multi (release-layer nav) XX (push-msg \"layer:base\"))",
true,
),
("(multi (release-layer nav) volu)", false),
("(multi (release-layer nav) calc)", false),
("(multi (release-layer nav) (unicode λ))", false),
("(release-key lctl)", false),
("(release-key volu)", false),
("(release-key calc)", false),
] {
let cfg = kanata_parser::cfg::new_from_str(
&format!(
Expand All @@ -605,14 +638,14 @@ mod tests {
for n in 0..index {
definitions.push_str(&format!("vk_{n} XX "));
}
definitions.push_str("vk_media volu)(defsrc a)(deflayer base a)");
definitions.push_str("vk_media calc)(defsrc a)(deflayer base a)");
let cfg = kanata_parser::cfg::new_from_str(&definitions, Default::default()).unwrap();
assert!(!supported(&cfg, &[4, 5]));
}

#[test]
fn session_virtual_switch_rejects_unsupported_virtual_key_action() {
let cfg = kanata_parser::cfg::new_from_str("(defvirtualkeys vk_test volu)(defalias kp-a (switch ((input virtual vk_test)) b break () a break))(defsrc a)(deflayer base @kp-a)", Default::default()).unwrap();
let cfg = kanata_parser::cfg::new_from_str("(defvirtualkeys vk_test calc)(defalias kp-a (switch ((input virtual vk_test)) b break () a break))(defsrc a)(deflayer base @kp-a)", Default::default()).unwrap();
assert!(!supported(&cfg, &[4, 5]));
}

Expand All @@ -627,7 +660,7 @@ mod tests {
#[test]
fn legacy_session_startup_unsupported() {
let cfg = kanata_parser::cfg::new_from_str(
"(defcfg alias-to-trigger-on-load boot)(defalias boot volu)(defsrc a)(deflayer base a)",
"(defcfg alias-to-trigger-on-load boot)(defalias boot calc)(defsrc a)(deflayer base a)",
Default::default(),
)
.unwrap();
Expand All @@ -647,7 +680,7 @@ mod tests {
#[test]
fn legacy_session_override_unsupported() {
let cfg = kanata_parser::cfg::new_from_str(
"(defoverrides (a) (volu))(defsrc a)(deflayer base a)",
"(defoverrides (a) (calc))(defsrc a)(deflayer base a)",
Default::default(),
)
.unwrap();
Expand All @@ -666,13 +699,13 @@ mod tests {

#[test]
fn legacy_session_chord_unsupported() {
let cfg = kanata_parser::cfg::new_from_str("(defcfg concurrent-tap-hold yes)(defchordsv2 (a b) volu 100 all-released ())(defsrc a b)(deflayer base a b)", Default::default()).unwrap();
let cfg = kanata_parser::cfg::new_from_str("(defcfg concurrent-tap-hold yes)(defchordsv2 (a b) calc 100 all-released ())(defsrc a b)(deflayer base a b)", Default::default()).unwrap();
assert_eq!(supported(&cfg, &[4, 5, 41, 224]), false);
}

#[test]
fn legacy_session_chord_nested_unsupported() {
let cfg = kanata_parser::cfg::new_from_str("(defcfg concurrent-tap-hold yes)(defchordsv2 (a b) (macro volu) 100 all-released ())(defsrc a b)(deflayer base a b)", Default::default()).unwrap();
let cfg = kanata_parser::cfg::new_from_str("(defcfg concurrent-tap-hold yes)(defchordsv2 (a b) (macro calc) 100 all-released ())(defsrc a b)(deflayer base a b)", Default::default()).unwrap();
assert_eq!(supported(&cfg, &[4, 5, 41, 224]), false);
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/KeyPathAppKit/Services/Import/QMKKeycodeMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@
// NX_KEYTYPE_PREVIOUS = 18
// NX_KEYTYPE_FAST = 19
// NX_KEYTYPE_REWIND = 20
// NX_KEYTYPE_BRIGHTNESS_UP = 21 (also 0xBD in QMK)
// NX_KEYTYPE_BRIGHTNESS_DOWN = 22 (also 0xBE in QMK)
// NX_KEYTYPE_ILLUMINATION_UP = 21 (keyboard backlight; also 0xBD in QMK)
// NX_KEYTYPE_ILLUMINATION_DOWN = 22 (display brightness is 2/3; see driverless-media-keys.md)
// NX_KEYTYPE_EJECT = 14
//
// These cannot be represented as standard CGKeyCode values.
Expand Down Expand Up @@ -244,7 +244,7 @@

// ── Mouse Movement ─────────────────────────────────────────
// swiftlint:disable:next line_length
add("M\u{2191}", "MS_UP") // M↑

Check warning on line 247 in Sources/KeyPathAppKit/Services/Import/QMKKeycodeMapping.swift

View workflow job for this annotation

GitHub Actions / code-quality

SwiftLint rule 'line_length' did not trigger a violation in the disabled region; remove the disable command (superfluous_disable_command)
add("M\u{2193}", "MS_DOWN") // M↓
add("M\u{2190}", "MS_LEFT") // M←
add("M\u{2192}", "MS_RGHT") // M→
Expand Down Expand Up @@ -296,7 +296,7 @@
/// Maps HID Usage Page 0x07 usage codes to macOS CGKeyCode values.
/// This is the underlying mapping that `qmkToMacOS` is built on,
/// indexed by numeric HID usage code instead of string name.
/// TODO: Wire up for future HID descriptor-based key identification (not used in current keymap-based flow).

Check warning on line 299 in Sources/KeyPathAppKit/Services/Import/QMKKeycodeMapping.swift

View workflow job for this annotation

GitHub Actions / code-quality

TODOs should be resolved (Wire up for future HID descrip...) (todo)
static let hidUsageToMacOS: [UInt16: UInt16] = [
// Letters (HID 0x04-0x1D)
0x04: 0x00, // A
Expand Down Expand Up @@ -454,6 +454,6 @@
0xE4: 0x3E, // Right Control
0xE5: 0x3C, // Right Shift
0xE6: 0x3D, // Right Alt/Option
0xE7: 0x36, // Right GUI/Command

Check warning on line 457 in Sources/KeyPathAppKit/Services/Import/QMKKeycodeMapping.swift

View workflow job for this annotation

GitHub Actions / code-quality

Collection literals should not have trailing commas (trailing_comma)
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,20 @@ public final class SessionRuntimeWorker {
}
continue
}
if event.usagePage == SessionKeyMap.consumerPage {
// Media keys: posted as system-defined events, held in
// their own ledger so a stop releases them too.
var nextOutputs = outputs
guard let media = try nextOutputs.translateMedia(event) else { continue }
if media.isDown { outputs = nextOutputs; writeReport(.running) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist held media usages in the crash ledger

When the worker crashes or is force-terminated while a media key is held, this writes a report after recording the media down, but writeReport serializes only outputs.heldUsages; recoverSessionOutputs likewise releases only report.heldOutputUsages. Because macOS repeats a posted media down until it receives an up, the missing media ledger can leave volume or brightness continuously changing after the worker dies. Include held consumer usages in SessionRuntimeReport and have the parent recovery path post their system-defined up events.

Useful? React with 👍 / 👎.

guard environmentObserver?.check() == true else {
finish(.failed, reason: "environment-observer-unavailable")
}
guard postMedia(media) else { finish(.failed, reason: "output-event-unavailable") }
outputCount += 1
if !media.isDown { outputs = nextOutputs; writeReport(.running) }
continue
}
var nextOutputs = outputs
let output = try nextOutputs.translate(event)
// Publish a press before posting it, but retain a release in
Expand Down Expand Up @@ -570,6 +584,24 @@ public final class SessionRuntimeWorker {
}
}

/// `NSEvent.otherEvent(.systemDefined, subtype 8)`: data1 carries the
/// NX key type in the high word and 0xA (down) / 0xB (up) in the next
/// byte. Tagged like a key event so this worker's own tap ignores it.
private func postMedia(_ output: SessionMediaOutput) -> Bool {
guard let event = Self.mediaEvent(keyType: output.keyType, isDown: output.isDown)?.cgEvent else { return false }
event.setIntegerValueField(.eventSourceUserData, value: Self.outputTag)
event.post(tap: .cgSessionEventTap)
return true
}

nonisolated static func mediaEvent(keyType: Int, isDown: Bool) -> NSEvent? {
NSEvent.otherEvent(
with: .systemDefined, location: .zero, modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: 0, context: nil,
subtype: 8, data1: (keyType << 16) | ((isDown ? 0xA : 0xB) << 8), data2: -1
)
}

private func post(_ output: SessionKeyOutput) -> Bool {
guard let source = CGEventSource(stateID: .privateState),
let event = CGEvent(keyboardEventSource: source, virtualKey: output.keyCode, keyDown: output.isDown)
Expand Down Expand Up @@ -736,14 +768,18 @@ public final class SessionRuntimeWorker {
/// The injected posting closure lets tests exercise this production loop
/// without creating a CGEvent or terminating a host process.
static func releaseOwnedOutputs(
_ outputs: inout SessionOutputState, post: (SessionKeyOutput) -> Bool
_ outputs: inout SessionOutputState, post: (SessionKeyOutput) -> Bool,
postMedia: (SessionMediaOutput) -> Bool = { _ in true }
) {
var pending = outputs
for output in pending.releaseAll() {
if post(output), let usage = SessionKeyMap.keyCodeToUsage[output.keyCode] {
_ = try? outputs.translate(.init(value: 0, usagePage: 7, usage: usage))
}
}
for media in pending.releaseAllMedia() where postMedia(media) {
_ = try? outputs.translateMedia(.init(value: 0, usagePage: SessionKeyMap.consumerPage, usage: media.usage))
}
}

private func finish(
Expand All @@ -763,7 +799,7 @@ public final class SessionRuntimeWorker {
// Allocation failure during shutdown cannot recursively exit before
// the power ACK. Preserve unposted releases in the crash ledger.
var remaining = outputs
Self.releaseOwnedOutputs(&remaining, post: post)
Self.releaseOwnedOutputs(&remaining, post: post, postMedia: postMedia)
outputs = remaining
if let owner = capsOwner, let directory = capsDirectory {
do {
Expand Down
28 changes: 26 additions & 2 deletions Sources/KeyPathCore/SessionRuntime/SessionKeyMap.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import Foundation

/// Physical macOS virtual key codes to USB keyboard usages, independent of the
/// active character layout. Fn and consumer keys deliberately remain unmapped:
/// session events cannot represent every HID key or identify its source device.
/// active character layout. Fn and most consumer keys remain unmapped: session
/// events cannot represent every HID key or identify its source device. The
/// media keys macOS carries as system-defined events are the exception, as
/// outputs (`mediaKeyType(forUsage:)`).
public enum SessionKeyMap {
public static let keyboardPage: UInt32 = 7
public static let consumerPage: UInt32 = 0x0C

/// Consumer-page usages the worker posts as `NSEvent` system-defined
/// (subtype 8) media events, by their `NX_KEYTYPE_*` code. The bridge's
/// output rule accepts exactly these (`CONSUMER_OUTPUT_CODES` in
/// session_config.rs); see docs/architecture/driverless-media-keys.md §2.
public static let consumerUsageToMediaKeyType: [UInt32: Int] = [
0xE9: 0, // volume up NX_KEYTYPE_SOUND_UP
0xEA: 1, // volume down NX_KEYTYPE_SOUND_DOWN
0x6F: 2, // brightness up NX_KEYTYPE_BRIGHTNESS_UP
0x70: 3, // brightness down
0xE2: 7, // mute NX_KEYTYPE_MUTE
0xCD: 16, // play/pause NX_KEYTYPE_PLAY
0xB5: 17, // next NX_KEYTYPE_NEXT
0xB6: 18, // previous NX_KEYTYPE_PREVIOUS
0xB3: 19, // fast-forward NX_KEYTYPE_FAST
0xB4: 20 // rewind NX_KEYTYPE_REWIND
]

public static func mediaKeyType(forUsage usage: UInt32) -> Int? {
consumerUsageToMediaKeyType[usage]
}

public static let keyCodeToUsage: [UInt16: UInt32] = [
0: 4, 1: 22, 2: 7, 3: 9, 4: 11, 5: 10,
Expand Down
43 changes: 43 additions & 0 deletions Sources/KeyPathCore/SessionRuntime/SessionOutputState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ public struct SessionKeyOutput: Equatable, Sendable {
public let flags: UInt64
}

/// A media key the worker posts as a system-defined event. No repeat: macOS
/// repeats a held media key itself (driverless-media-keys.md §2c).
public struct SessionMediaOutput: Equatable, Sendable {
public let usage: UInt32
/// `NX_KEYTYPE_*`, from `SessionKeyMap.mediaKeyType(forUsage:)`.
public let keyType: Int
public let isDown: Bool

public init(usage: UInt32, keyType: Int, isDown: Bool) {
self.usage = usage
self.keyType = keyType
self.isDown = isDown
}
}

/// Tracks only keys actually emitted by this runtime. Failure cleanup releases
/// that set, never a guessed list of every key on the user's keyboard.
public struct SessionOutputState: Sendable {
Expand All @@ -29,9 +44,37 @@ public struct SessionOutputState: Sendable {
}

public private(set) var heldUsages: Set<UInt32> = []
/// Consumer-page media keys currently down, released like keys on cleanup.
public private(set) var heldMediaUsages: Set<UInt32> = []

public init() {}

/// A consumer-page event as the media output to post, or nil for a repeat
/// (dropped: the system repeats a held media key on its own).
public mutating func translateMedia(_ event: KanataHostBridgePassthruOutputEvent) throws -> SessionMediaOutput? {
guard event.usagePage == SessionKeyMap.consumerPage,
let keyType = SessionKeyMap.mediaKeyType(forUsage: event.usage), event.value <= 2
else {
throw Failure.unsupportedEvent(page: event.usagePage, usage: event.usage, value: event.value)
}
if event.value == 2 {
guard heldMediaUsages.contains(event.usage) else { throw Failure.repeatWithoutPress(usage: event.usage) }
return nil
}
if event.value == 0 {
heldMediaUsages.remove(event.usage)
} else {
heldMediaUsages.insert(event.usage)
}
return SessionMediaOutput(usage: event.usage, keyType: keyType, isDown: event.value != 0)
}

public mutating func releaseAllMedia() -> [SessionMediaOutput] {
heldMediaUsages.sorted().compactMap { usage in
try? translateMedia(.init(value: 0, usagePage: SessionKeyMap.consumerPage, usage: usage)) ?? nil
}
}

public var modifierFlags: UInt64 {
heldUsages.reduce(0) { $0 | SessionKeyMap.modifierFlags(for: $1) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ final class AppSpecificSourceRoutingTests: XCTestCase {
for (output, expectedValid) in [
("d", true),
("(switch ((input virtual vk_test_app)) (unicode ä) break () d break)", false),
("volu", false)
("volu", true), // phase 6: media keys are session outputs
("fn", false)
] {
let keymap = AppKeymap(bundleIdentifier: "test.app", displayName: "Test App", overrides: [
AppKeyOverride(inputKey: "c", action: .rawKanata(output))
Expand Down
23 changes: 11 additions & 12 deletions Tests/KeyPathTests/Managers/AppKeymapSaveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
mutate: { keymaps in
keymaps[0].overrides.append(AppKeyOverride(inputKey: "c", action: .rawKanata("(unicode ä)")))
}, runtimeDidApply: { refreshed = true }
) {

Check warning on line 63 in Tests/KeyPathTests/Managers/AppKeymapSaveTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
reloads += 1
return Self.reload(.applied)
}
Expand All @@ -79,7 +79,7 @@
try await withFixture { fixture in
let result = await fixture.coordinator.saveAppKeymaps(store: fixture.store, mutate: { keymaps in
keymaps[0].overrides.append(AppKeyOverride(inputKey: "a", action: .keystroke(key: "z")))
}) {

Check warning on line 82 in Tests/KeyPathTests/Managers/AppKeymapSaveTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
XCTFail("Invalid source must not reach runtime")
return Self.reload(.applied)
}
Expand Down Expand Up @@ -109,7 +109,7 @@
XCTAssertEqual(reloads, 1)
XCTAssertEqual(applied, 1)
throw CancellationError()
}, runtimeDidApply: { applied += 1 }) {

Check warning on line 112 in Tests/KeyPathTests/Managers/AppKeymapSaveTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
reloads += 1
do { try self.assertOriginalFiles(fixture) } catch { XCTFail("\(error)") }
return Self.reload(.applied)
Expand All @@ -127,7 +127,7 @@
try await self.interruptAppWrite(fixture)
let failed = await fixture.coordinator.saveAppKeymaps(store: fixture.store, mutate: { _ in
XCTFail("Must not edit while recovery is rejected")
}) { Self.reload(.rejected) }

Check warning on line 130 in Tests/KeyPathTests/Managers/AppKeymapSaveTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
XCTAssertFalse(failed.success)
XCTAssertTrue(failed.error?.localizedDescription.contains("Recovered files could not be applied") == true)
XCTAssertFalse(FileManager.default.fileExists(atPath: RecoverableRuleWrite.journalURL(fixture.directory, scope: .appKeymaps).path))
Expand Down Expand Up @@ -392,21 +392,20 @@
}
}

// Stored media profiles remain intact when an app edit cannot run them.
// Generate them for inspection, but require the real validator to reject
// before any app source, include, main config, or journal is written.
for selected in [RuleCollectionIdentifier.macFunctionKeys] {
// A stored rule the session cannot run (Fn as an output: the generator
// passes the name through and the bridge refuses it; media keys are
// posted since phase 6) stays intact when an app edit cannot run
// alongside it. Generate for inspection, but require the real validator
// to reject before any app source, include, main config, or journal is
// written.
let unsupportedRule = CustomRule(input: "f9", action: .keystroke(key: "fn"))
for selected in ["custom rule f9 → fn"] {
let collections = RuleCollectionCatalog().defaultCollections().map { original in
var collection = original
collection.isEnabled = collection.id == selected
if collection.id == RuleCollectionIdentifier.macFunctionKeys {
collection.functionKeyMode = .media
collection.mappings = RuleCollectionCatalog.functionKeyMappings(for: .media)
}
collection.isEnabled = false
return collection
}
XCTAssertTrue(collections.contains { $0.id == selected && $0.isEnabled })
try await withFixture(collections: collections, customRules: [custom]) { fixture in
try await withFixture(collections: collections, customRules: [custom, unsupportedRule]) { fixture in
var reloads = 0
var sourceBefore: [String: Data] = [:]
for name in ["RuleCollections.json", "CustomRules.json"] {
Expand All @@ -418,7 +417,7 @@
reloads += 1
return Self.reload(.applied)
}
XCTAssertFalse(result.success, "\(selected) must reject unsupported media output")
XCTAssertFalse(result.success, "\(selected) must reject an unsupported output")
XCTAssertTrue(result.error?.localizedDescription.contains("driverless session") == true)
try self.assertOriginalFiles(fixture)
for (name, bytes) in sourceBefore {
Expand Down
5 changes: 3 additions & 2 deletions Tests/KeyPathTests/Managers/SaveCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@
XCTAssertEqual(try String(contentsOf: url, encoding: .utf8), original)
}

/// AI/import/editor text must pass driverless admission, not just Kanata syntax validation.
/// AI/import/editor text must pass driverless admission, not just Kanata syntax
/// validation. Fn is a key the session cannot emit (media keys are posted since phase 6).
func testGeneratedConsumerOutputRejectsBeforePersistingOrReloading() async throws {
try await assertDriverlessGeneratedConfigRejected(
"(defcfg)\n(defsrc a)\n(deflayer base volu)"
"(defcfg)\n(defsrc a)\n(deflayer base fn)"
)
}

Expand Down Expand Up @@ -179,7 +180,7 @@
let result = await coordinator.saveGeneratedConfig(
content: candidate,
runtimeDidApply: { runtimeApplyCount += 1 }
) {

Check warning on line 183 in Tests/KeyPathTests/Managers/SaveCoordinatorTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
reloadCount += 1
return ReloadResult(success: true, response: "ok", errorMessage: nil, protocol: nil)
}
Expand Down Expand Up @@ -212,7 +213,7 @@
let result = await coordinator.saveGeneratedConfig(
content: candidate,
runtimeDidApply: { runtimeApplyCount += 1 }
) {

Check warning on line 216 in Tests/KeyPathTests/Managers/SaveCoordinatorTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
reloadCount += 1
XCTAssertEqual(try? String(contentsOf: url, encoding: .utf8), candidate)
return ReloadResult(success: true, response: "ok", errorMessage: nil, protocol: nil)
Expand Down Expand Up @@ -447,7 +448,7 @@
operation = Task { @MainActor in
await self.coordinator.saveMapping(input: "c", output: "d", ruleCollectionsManager: manager) {
reloadCount += 1
if reloadCount == 1 { operation?.cancel() }

Check warning on line 451 in Tests/KeyPathTests/Managers/SaveCoordinatorTests.swift

View workflow job for this annotation

GitHub Actions / code-quality

Else and catch should be on the same line, one space after the previous declaration (statement_position)
else {
XCTAssertFalse(Task.isCancelled, "Recovery must survive caller cancellation")
do {
Expand Down
Loading
Loading