Skip to content

Add testing framework, fix issues - #83

Merged
HaoboGu merged 10 commits into
mainfrom
fix/link-lifecycle-and-tests
Aug 3, 2026
Merged

Add testing framework, fix issues#83
HaoboGu merged 10 commits into
mainfrom
fix/link-lifecycle-and-tests

Conversation

@HaoboGu

@HaoboGu HaoboGu commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added user-initiated Web Serial connection flow with clearer picker and connection handling.
    • Added connection status messages for connecting, disconnected, unavailable, and lost-link states.
    • Added keyboard controls for lock status, Bluetooth profiles, storage reset, reboot, and bootloader access.
    • Added support for displaying connection type, Bluetooth status, and lock-aware keyboard state.
  • Bug Fixes

    • Improved cleanup and recovery when device connections fail or are interrupted.
  • Documentation

    • Added comprehensive testing and troubleshooting guidance.

coderabbitai[bot]

This comment was marked as low quality.

Repository owner deleted a comment from coderabbitai Bot Aug 1, 2026
Repository owner deleted a comment from coderabbitai Bot Aug 1, 2026
Repository owner deleted a comment from coderabbitai Bot Aug 1, 2026
Repository owner deleted a comment from coderabbitai Bot Aug 1, 2026
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

HaoboGu added a commit that referenced this pull request Aug 1, 2026
- ci: least-privilege permissions, non-persisted checkout credentials, and a
  pinned cargo-binstall SHA.
- ci/qemu: clone rmk once and point both the firmware and build:wasm at it via
  RMK_REPO, so the two artifacts cannot land on different protocol commits.
  qemu/run.mjs now resolves rmk the same way build-rynk-wasm.py does.
- qemu: the smoke test allocates a free port and hands it to run.mjs instead of
  sharing a hardcoded 7965.
- rynk: probeVersion takes a deadline, so a device that opens the port and never
  answers surfaces a transport error instead of parking the store in connecting.
- store: endSession awaits the teardown and runs it on the failure path too;
  a not-connected reboot no longer relabels a link-lost phase.
- ui: the picker distinguishes dismissal from a real failure and surfaces both
  it and a failed handshake; StateBar matches phases exhaustively and shows the
  error cause.
- tests: cover the session-ending, storage-reset and BLE commands, the probe
  deadline, and derive the rynk code list from its source of truth.
@HaoboGu

HaoboGu commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review in 14d0051. All nine points were valid; each is fixed below.

Actionable

1. .github/workflows/ci.yml — QEMU job credentials. Added a top-level permissions: contents: read (the deploy job keeps its own pull-requests: write), persist-credentials: false on all three checkouts, and pinned cargo-bins/cargo-binstall to e00d2c94 (v1.21.1).

2. .github/workflows/ci.yml — one rmk revision. Worse than reported: qemu/Cargo.lock had gone stale (its rmk entry carried no source), so cargo re-resolved branch = "main" at build time with nothing pinning it. Rather than patching this in CI only, qemu/run.mjs now resolves rmk with the same order as scripts/build-rynk-wasm.pyRMK_REPO, then a sibling ../rmk — and passes the resolved checkout to cargo as --config patch."https://github.com/rmk-rs/rmk.git".{rmk,rmk-config}.path=…. CI clones once into $RUNNER_TEMP/rmk, prints the SHA, and exports RMK_REPO for both build:wasm and test:qemu.

This also closes the same hole locally: a sibling ../rmk was already used for the wasm client but not for the firmware. Verified that all four rmk* crates resolve to the single checkout and that qemu/Cargo.lock stays unchanged under the patch.

3. qemu/smoke.test.ts — hardcoded port. The test now asks the OS for a free port and passes it as RMK_QEMU_PORT; run.mjs reads it and defaults to 7965 for a manual pnpm qemu. Note the fix could not go through argv — run.mjs forwards process.argv.slice(2) straight to cargo.

Nitpicks

src/App.svelte — only DOMException NotFoundError is swallowed as dismissal; anything else, plus an Err from initStore, renders under the connect button.

src/rynk/core.tsprobeVersion takes a deadline (10s default, overridable via connectClient). Timeout and EOF both throw with name = 'TransportError', so toKeyboardError classifies them as transport rather than unknown.

src/stores/keyboard/keyboard.svelte.tsendSession awaits the teardown via andThrough and repeats it in orElse, so a non-transport failure no longer leaves a connected phase. One deviation from the suggested diff: invalid (the not-connected guard) is excluded, because otherwise a reboot attempted on an already-dead link would relabel error/"link lost" as disconnected. There is a test for that.

src/components/StateBar.sveltematch(c.phase).…exhaustive(); a new ConnectionPhase now fails the build. cause is surfaced through a new describeKeyboardError.

src/stores/keyboard/errors.test.tsRYNK_ERROR_CODES is exported and the test iterates Object.keys on it.

src/stores/keyboard/keyboard.test.ts — added suites for reboot, bootloaderJump, storageReset, refreshBleStatus, switchBleProfile, clearBleProfile, including the teardown-on-failure and dead-link paths, plus a probe-deadline test.

pnpm test 43 passed, pnpm check 0 errors, CI=true pnpm lint clean, and pnpm test:qemu passes end-to-end locally against the riscv fixture.

@github-actions

This comment was marked as outdated.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Comment thread src/rynk/index.ts

/// Web mode cannot enumerate: `navigator.serial.requestPort()` needs a user
/// gesture and opens the browser's own picker, so callers use connectWebSerial.
export function canDiscover(): boolean {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

感觉这函数意义不大啊

@HaoboGu HaoboGu Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

#86 里移除了

HaoboGu added 10 commits August 3, 2026 01:07
cobsEncode never closed a full 0xFF block, so `code` overflowed 255 and was
truncated by Uint8Array — any payload with a zero-free run longer than 254
bytes encoded to garbage. cobsDecode already handled the 0xFF case.

Latent today (the only encode site is the 3-byte GetVersion probe), but the
helpers are now exported as protocol primitives for tests.

connectClient takes an optional wasm source: the browser default resolves the
module next to the JS glue, which Node fetch cannot do for a file: URL.
A dead link used to leave the store looking connected: the topic loop swallowed
its exception and nothing called resetStore, so the UI kept an editable keymap
until the next write failed. Both death paths — a parked next_topic() rejecting
and any request returning a transport error — now converge on teardown, which
clears device/config/status and reports { phase: error, cause }.

teardown takes fromTopicLoop because awaiting session.topicLoop from inside that
loop deadlocks on its own promise.

Also fixes connecting to a locked device: GetMatrixState is unlock-gated
upstream, and fetchStatus asked for it unconditionally, failing the whole
handshake. It is now skipped while locked and read back after unlockPoll.

New commands: refreshStatus, refreshLockStatus, lock, unlockPoll,
refreshBleStatus, switchBleProfile, clearBleProfile, reboot, bootloaderJump,
storageReset. Reboot and bootloader jump are fire-and-forget upstream, so both
the ack and a transport failure end the session.

KeyboardError missed the Busy variant; RYNK_ERROR_CODES is now a Record so a new
upstream variant fails the build.
web.ts was dead code: index.ts imported WebByteLink with `import type`, so it
never existed at runtime, and discover() returns [] outside Tauri. Web mode
could not reach a device at all.

connectWebSerial/connectWebHid are now real exports, and canDiscover() names the
Tauri-only enumeration so App can offer a click-driven picker instead — the
browser requires a user gesture for requestPort().
CI only ran lint and build, and the qemu fixture had no consumer.

26 unit tests cover the hand-rolled wire code (COBS round-trips including runs
past 254 bytes, chunk reassembly, interleaved topic pushes), the rynk error
mapping, and the store: optimistic rollback, request serialization, teardown on
link death, connecting while locked, and the unlock ceremony.

6 smoke tests drive the real riscv fixture firmware through the real rynk-wasm
client over the UART-to-TCP bridge. They live behind pnpm test:qemu because they
need qemu-system-riscv32 and a riscv target, and run as a Linux-only CI job.
deploy now waits on it — a protocol regression should not ship.

CI also runs pnpm check, which was never wired up.
The npm-scripts block is a fragment of object properties, not a JSON document,
so the jsonc parser eslint runs over fenced blocks failed on the first colon.
Retagged as text — wrapping it in braces parses but then trips
style/no-multi-spaces on the aligned values, and --fix would drop the alignment.
Every command in the Development section was inverted — web:dev/tauri:dev/
web:build/tauri:build, none of which exist; the scripts are dev:web, dev:tauri,
build:web, build:tauri. Following the README got you four failures in a row.

It also omitted build:wasm entirely. src/rynk/wasm/ is gitignored, so a fresh
clone has no protocol client and dev:web fails on the first import.

Notes that dev:tauri and build:tauri run their web counterparts through
tauri.conf beforeDevCommand/beforeBuildCommand, so starting both duplicates the
vite server.

Adds a Testing section. test:qemu spawns the fixture itself, so it conflicts
with a hand-started pnpm qemu — the serial port takes one client. Also flags
CI=true for lint: the eslint config relaxes rules when it detects an editor.
The header, features and acknowledgements still described Nuxt and PrimeVue.
Neither is a dependency — nor is Vue at all. The UI is Svelte 5 with Tailwind 4
and Ark UI.

Also fixes the icon alt text (left over from Clash), an unclosed <h3>, and a
missing period in Install.
- ci: least-privilege permissions, non-persisted checkout credentials, and a
  pinned cargo-binstall SHA.
- ci/qemu: clone rmk once and point both the firmware and build:wasm at it via
  RMK_REPO, so the two artifacts cannot land on different protocol commits.
  qemu/run.mjs now resolves rmk the same way build-rynk-wasm.py does.
- qemu: the smoke test allocates a free port and hands it to run.mjs instead of
  sharing a hardcoded 7965.
- rynk: probeVersion takes a deadline, so a device that opens the port and never
  answers surfaces a transport error instead of parking the store in connecting.
- store: endSession awaits the teardown and runs it on the failure path too;
  a not-connected reboot no longer relabels a link-lost phase.
- ui: the picker distinguishes dismissal from a real failure and surfaces both
  it and a failed handshake; StateBar matches phases exhaustively and shows the
  error cause.
- tests: cover the session-ending, storage-reset and BLE commands, the probe
  deadline, and derive the rynk code list from its source of truth.
Web Serial's getInfo() exposes no string descriptors, so connectWebSerial can
only offer the constant 'WebSerial' and the status bar showed that instead of
the device. The handshake already reads product_name via get_device_info; use it
for the connected phase and keep the transport label as the fallback and for the
pre-handshake phases.
qemu binds the serial socket during machine init, so the harness could connect
and send GET_VERSION before the guest booted — and Uart::new() resets the 16550
FIFO, discarding it. The fixture never answered and the probe burned its whole
deadline; CI failed this way once out of several runs.

The fixture now prints a marker once its UART is up and the harness waits for it
before dialling. Kept out of probeVersion on purpose: a real keyboard is long
past boot by the time a host connects.

Also hold run.mjs's output and dump it when the handshake fails — the previous
run was undiagnosable because cargo and semihosting output went to /dev/null.
@HaoboGu
HaoboGu force-pushed the fix/link-lifecycle-and-tests branch from fc98c56 to 5c912a0 Compare August 2, 2026 17:15
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@HaoboGu
HaoboGu merged commit 1aa5dc6 into main Aug 3, 2026
16 checks passed
@HaoboGu
HaoboGu deleted the fix/link-lifecycle-and-tests branch August 3, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants