Skip to content

fix(nextcloud-talk): keep error snippets UTF-16 safe#102949

Merged
steipete merged 2 commits into
openclaw:mainfrom
mushuiyu886:fix/followup-102833
Jul 9, 2026
Merged

fix(nextcloud-talk): keep error snippets UTF-16 safe#102949
steipete merged 2 commits into
openclaw:mainfrom
mushuiyu886:fix/followup-102833

Conversation

@mushuiyu886

@mushuiyu886 mushuiyu886 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep Nextcloud Talk error-body snippets UTF-16 safe when a long response body is collapsed for user-visible errors.
  • Add a local HTTP-server regression through sendMessageNextcloudTalk so the failing body crosses the real send/error path.

Maintainer verification

  • Frozen head: 33c26657224b5af89484d33915c165f311b6ca29 (includes an exact thrown-Error assertion for the malformed-surrogate regression).
  • Exact-head CI: run 29030125468 completed successfully: 59 checks succeeded, one CodeQL check was neutral, 30 routine checks were skipped, and none failed or remained pending.
  • Sanitized AWS green: run run_fff37b03fe84 fetched the remote PR at the frozen SHA through the trusted untrusted-source bootstrap and passed the complete Nextcloud Talk suite: 16 files, 101 tests.
  • Sanitized AWS control red: run run_32cee4fec874 started from the same frozen SHA, replaced only the helper call with the former raw slice(0, 200) behavior in its ephemeral remote worktree, and the focused regression failed as expected: one failed, one passed. The received message contained the split-surrogate replacement character. The mutated lease was discarded.
  • Fresh autoreview: clean, no actionable findings, 0.91 confidence.
  • Known live-proof gap: no external Nextcloud instance was used. The regression crosses a real local HTTP request/response boundary through the exported send API; the changed behavior is the local post-response formatter.

What Problem This Solves

Nextcloud Talk error responses are bounded and then shortened before being included in send/reaction errors. The old formatter used slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS), so a response body with an emoji or other astral character starting at the truncation boundary could leave a lone UTF-16 surrogate in the thrown error message.

Latest origin/main still has the raw slice at extensions/nextcloud-talk/src/send.ts:

origin/main=4f9a371c9530e9d2eb2912a44054a44228a59612
32:    return `${collapsed.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;

User Impact

  • Why it matters / User impact: Users sending to a Nextcloud Talk room can receive malformed or replacement-character snippets in the CLI/runtime error when the self-hosted server returns a long Unicode error body. The fix keeps the snippet readable and avoids surfacing half of a surrogate pair.

Origin / follow-up

Follows #102833.

  • Follows: fix(lifecycle): keep install output truncation UTF-16 safe #102833 fixed the same UTF-16 boundary class for install failure summaries by replacing raw prefix slicing with truncateUtf16Safe.
  • Gap: Nextcloud Talk retained the same raw prefix truncation in its error-body snippet formatter.
  • Consistency: This applies the same shared UTF-16-safe truncation pattern to the channel error path without changing request construction, status handling, or body byte limits.

Competition / linked PR analysis

  • Related open PR scan: checked open PRs for nextcloud-talk UTF-16 error snippet in openclaw/openclaw before submission.
[]

No open same-point PR was found.

Merge risk

  • Risk labels considered: message-delivery, availability, compatibility, security-boundary, session-state, automation.
  • Risk explanation: The only relevant risk surfaces for this patch are message-delivery and availability because it touches extensions/nextcloud-talk/src/send.ts. The change runs after an HTTP response is already non-OK and only changes the formatter for the bounded response snippet. It does not alter delivery requests, auth/signing, timeout behavior, success parsing, state/session handling, security policy, or background execution.
  • Why acceptable: The diff is limited to one truncation helper call and one regression test. The byte cap remains enforced by readResponseTextLimited; the thrown error still includes the same status-specific prefix and ellipsis. The only behavior change is avoiding invalid UTF-16 when the snippet limit lands inside an astral code point.
  • Patch quality notes: Focused source change plus a regression that crosses the exported channel send path with a local HTTP server. No skip/only directives, no type escapes, no fallback/default strategy, no package dependency change, and no unrelated cleanup.
  • Maintainer-ready confidence: High: latest main still has the raw slice, there is no open same-point PR, the fix uses an existing shared helper, and the local server proof exercises the user-visible error path.

Real behavior proof

  • Behavior or issue addressed: A long Nextcloud Talk error response whose 200th UTF-16 code unit lands inside an emoji should not surface a malformed half-surrogate in the thrown send error.
  • Canonical reachability path: user/config/input → schema/type/ingestion/normalization → runtime object → request/effect: user target/message/config input (room:abc123, hello, channels.nextcloud-talk.baseUrl, botSecret, and private-network opt-in) is accepted as CoreConfig by sendMessageNextcloudTalk, resolved by resolveNextcloudTalkSendContext / resolveNextcloudTalkAccount into runtime account/baseUrl/secret, sent as a signed POST request to /ocs/v2.php/apps/spreed/api/v1/bot/abc123/message, receives a non-OK Unicode response body from the local Nextcloud-compatible endpoint, passes through readResponseTextLimited and collapseErrorSnippet, then becomes the thrown user-visible Nextcloud Talk: bad request - ... effect.
  • Boundary crossed: The regression uses withServer to run an actual local HTTP server, receives the real POST from sendMessageNextcloudTalk, returns HTTP 400 with a Unicode text body, and observes the thrown error from the exported send API.
  • Shared helper / provider constraint check: The fix uses truncateUtf16Safe from openclaw/plugin-sdk/text-utility-runtime, the existing SDK text helper backed by OpenClaw's UTF-16-safe truncation utility. The existing readResponseTextLimited byte budget remains unchanged.
  • Real environment tested: Linux local worktree, Node/Vitest, local HTTP server via openclaw/plugin-sdk/test-env; no mocked fetch in the regression proof.
  • Exact steps or command run after this patch:
source /media/vdb/code/ai/aispace/openclaw-followup-102833-evidence/context.env
cd "$TASK_WORKTREE"
node scripts/run-vitest.mjs extensions/nextcloud-talk/src/send.fetch-timeout.test.ts
node scripts/run-vitest.mjs extensions/nextcloud-talk/src/send.cfg-threading.test.ts
  • Evidence after fix:
[test] starting test/vitest/vitest.extension-messaging.config.ts

 Test Files  1 passed (1)
      Tests  2 passed (2)
...
[test] passed 1 Vitest shard in 24.02s
[test] starting test/vitest/vitest.extension-messaging.config.ts

 Test Files  1 passed (1)
      Tests  12 passed (12)
...
[test] passed 1 Vitest shard in 17.65s
origin/main=4f9a371c9530e9d2eb2912a44054a44228a59612
32:    return `${collapsed.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
  • Observed result after fix: The local server regression exercises the exported send path and expects the thrown message to be exactly Nextcloud Talk: bad request - ${prefix}… for prefix = "e".repeat(199), proving the emoji that straddles the boundary is dropped instead of leaving a lone surrogate.
  • What was not tested: I did not use a real external Nextcloud instance. The changed behavior is the local formatter for any non-OK HTTP response body after the response has already crossed the fetch boundary, and the proof covers that boundary with a local HTTP server.
  • Fix classification: Root cause fix

Review findings addressed

No maintainer review findings exist for this new follow-up branch. Pre-submit checks covered the known follow-up risks: latest main still has the raw slice gap, no same-point open PR was found, and the regression crosses the exported send path with a local HTTP server.

Regression Test Plan

  • Target test file: extensions/nextcloud-talk/src/send.fetch-timeout.test.ts for the new local-server UTF-16-safe error snippet regression; extensions/nextcloud-talk/src/send.cfg-threading.test.ts for existing send/reaction error-path coverage.
  • Scenario locked in: A 400 response body constructed as "e".repeat(199) + "\u{1F600}tail" crosses the real POST path and then hits the snippet limit at the emoji's surrogate pair boundary.
  • Why this is the smallest reliable guardrail: It asserts the exported send API's thrown error after a real local HTTP response, so it covers request construction, response handling, bounded body reading, snippet formatting, and user-visible error propagation without requiring unrelated Nextcloud infrastructure.
node scripts/run-vitest.mjs extensions/nextcloud-talk/src/send.fetch-timeout.test.ts
node scripts/run-vitest.mjs extensions/nextcloud-talk/src/send.cfg-threading.test.ts
git show origin/main:extensions/nextcloud-talk/src/send.ts | grep -n "collapsed\.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)"

Risk / Compatibility

The output remains bounded and still appends the same ellipsis for overlong snippets. The only observable change is that truncation will not split a surrogate pair; snippets may be one UTF-16 code unit shorter when the limit lands inside an astral character.

What was not changed

  • What did NOT change: Does not change Nextcloud Talk auth, signature generation, request body construction, room token normalization, SSRF policy, timeout handling, success response parsing, reaction status handling, the error-body byte cap, or package dependencies.
  • Architecture / source-of-truth check: Source-of-truth boundary: collapseErrorSnippet is the canonical source formatter used by readNextcloudTalkErrorSnippet before both send and reaction errors are thrown. This patch fixes that formatter rather than post-processing a mirror, projection, log copy, or test-only representation.

Root Cause

  • Root cause: Root cause is the source formatter collapseErrorSnippet shortening collapsed error bodies with raw JavaScript slice(0, 200). Because JavaScript string indices are UTF-16 code units, slicing at an arbitrary code-unit boundary can cut between the high and low surrogate of an emoji or another astral code point.
  • Why this is root-cause fix: The patch replaces the raw code-unit slice at the single snippet formatter with the shared UTF-16-safe truncation helper. Every non-OK Nextcloud Talk response body still flows through the same formatter, so the invalid-surrogate source is removed before the message is assembled.

@openclaw-barnacle openclaw-barnacle Bot added channel: nextcloud-talk Channel integration: nextcloud-talk size: XS labels Jul 9, 2026
@steipete steipete self-assigned this Jul 9, 2026
@clawsweeper

clawsweeper Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 9, 2026, 12:18 PM ET / 16:18 UTC.

Summary
The PR replaces the Nextcloud Talk error snippet raw prefix slice with the SDK UTF-16-safe truncation helper and adds a local HTTP-server regression for the exported send path.

PR surface: Source +1, Tests +25. Total +26 across 2 files.

Reproducibility: yes. from source: current main slices collapsed Nextcloud Talk error bodies with slice(0, 200), and the send path throws that snippet for 400 responses. The PR's local-server regression demonstrates the same exported send path after the fix.

Review metrics: none identified.

Merge readiness
Overall: 🦀 challenger crab
Proof: 🦀 challenger crab
Patch quality: 🦀 challenger crab
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] GitHub currently reports the PR as mergeable but behind the current base; maintainers may want a base refresh or exact merge-head validation before landing.

Maintainer options:

  1. Decide the mitigation before merge
    Land the focused formatter/test fix after normal exact-head merge gating, refreshing the branch first only if maintainers require latest-base validation.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • No ClawSweeper repair is needed; the remaining action is normal maintainer review, base freshness, and exact-head merge gating.

Security
Cleared: No security or supply-chain concern found: the diff only changes bounded local error text formatting and a regression test, with no dependency, workflow, secret, auth, SSRF, or request-routing change.

Review details

Best possible solution:

Land the focused formatter/test fix after normal exact-head merge gating, refreshing the branch first only if maintainers require latest-base validation.

Do we have a high-confidence way to reproduce the issue?

Yes from source: current main slices collapsed Nextcloud Talk error bodies with slice(0, 200), and the send path throws that snippet for 400 responses. The PR's local-server regression demonstrates the same exported send path after the fix.

Is this the best way to solve the issue?

Yes. The formatter is the shared source for send and reaction error snippets, and replacing only its prefix slice with the existing SDK helper preserves request, auth, SSRF, status handling, and byte-limit behavior.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 148ec3282f07.

Label changes

Label changes:

  • add rating: 🦀 challenger crab: Overall readiness is 🦀 challenger crab; proof is 🦀 challenger crab and patch quality is 🦀 challenger crab.
  • remove rating: 🦞 diamond lobster: Current PR rating is rating: 🦀 challenger crab, so this older rating label is no longer current.

Label justifications:

  • P2: The PR fixes a bounded Nextcloud Talk user-visible error-message bug with limited blast radius and no config, auth, delivery, or dependency changes.
  • rating: 🦀 challenger crab: Overall readiness is 🦀 challenger crab; proof is 🦀 challenger crab and patch quality is 🦀 challenger crab.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body and maintainer comments provide after-fix terminal output from local HTTP-server runs through sendMessageNextcloudTalk, plus a control-red run reintroducing the raw slice; no contributor action is needed.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body and maintainer comments provide after-fix terminal output from local HTTP-server runs through sendMessageNextcloudTalk, plus a control-red run reintroducing the raw slice; no contributor action is needed.
Evidence reviewed

PR surface:

Source +1, Tests +25. Total +26 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 2 1 +1
Tests 1 25 0 +25
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 27 1 +26

What I checked:

  • Current main still has unsafe truncation: collapseErrorSnippet collapses the response body and then returns collapsed.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS), so a surrogate pair can be split at the 200-code-unit boundary before send/reaction errors are thrown. (extensions/nextcloud-talk/src/send.ts:32, 148ec3282f07)
  • Formatter is shared by send and reaction failures: readNextcloudTalkErrorSnippet calls the formatter, and both sendMessageNextcloudTalk and sendReactionNextcloudTalk use that snippet for non-OK HTTP responses, so fixing this helper covers the relevant Nextcloud Talk error source. (extensions/nextcloud-talk/src/send.ts:38, 148ec3282f07)
  • PR changes only the formatter call and regression test: The PR imports truncateUtf16Safe from openclaw/plugin-sdk/text-utility-runtime and replaces only the formatter prefix slice; the diff also adds a local HTTP-server test through sendMessageNextcloudTalk. (extensions/nextcloud-talk/src/send.ts:30, 33c26657224b)
  • SDK helper contract is surrogate-safe: truncateUtf16Safe floors the requested limit and delegates to sliceUtf16Safe, which backs off when the truncation end would leave a high surrogate without its low surrogate. (packages/normalization-core/src/utf16-slice.ts:44, 148ec3282f07)
  • Scoped plugin policy supports this import boundary: The extensions policy says bundled plugin production code should import through openclaw/plugin-sdk/*; this PR uses that public SDK route rather than core internals. (extensions/AGENTS.md:23, 148ec3282f07)
  • History points to the current Nextcloud Talk send implementation: Blame for the formatter and bounded error-snippet logic points to commit 2f01338, which added the current Nextcloud Talk send module on main. (extensions/nextcloud-talk/src/send.ts:32, 2f013382a0c1)

Likely related people:

  • steipete: Blame ties the current Nextcloud Talk send/error-snippet implementation to commit 2f01338, live PR metadata shows steipete assigned, and the latest branch commit/refinement plus maintainer verification came from steipete. (role: recent area contributor and reviewer; confidence: high; commits: 2f013382a0c1, 33c26657224b; files: extensions/nextcloud-talk/src/send.ts, extensions/nextcloud-talk/src/send.fetch-timeout.test.ts)
  • LeonidasLux: The linked merged lifecycle PR fixed the same UTF-16-safe truncation class in another surface, which is useful context for the shared helper pattern but not ownership of the Nextcloud Talk send path. (role: adjacent bug-class contributor; confidence: medium; commits: 20646156a762; files: src/skills/lifecycle/install-output.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-07-09T15:43:32.476Z sha 33c2665 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jul 9, 2026
@steipete

steipete commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Maintainer verification complete for exact head 33c26657224b5af89484d33915c165f311b6ca29.

  • Best-fix check: this is the right owner boundary and the leanest fix. collapseErrorSnippet is the one formatter shared by send and reaction failures, and truncateUtf16Safe is already the public plugin-SDK contract for this exact surrogate-boundary invariant. Moving the behavior into the byte reader, adding a Nextcloud-only helper, or post-processing individual errors would broaden or duplicate policy.
  • Test refinement: the regression now compares the complete thrown Error, so a dangling surrogate cannot pass via substring matching.
  • Sanitized AWS green: run run_fff37b03fe84 passed the complete Nextcloud Talk suite: 16 files, 101 tests.
  • Sanitized AWS control red: run run_32cee4fec874 started from the same exact SHA, reintroduced only the former raw slice(0, 200) call in its ephemeral remote worktree, and the focused regression failed as expected: one failed, one passed; the received message showed the split-surrogate replacement character.
  • Exact-head CI: run 29030125468 succeeded with no pending or failing relevant checks.
  • Fresh autoreview: clean, no actionable findings, 0.91 confidence.

No external Nextcloud instance was used. The regression does cross a real local HTTP request/response boundary through sendMessageNextcloudTalk; the changed behavior is the local post-response formatter and does not depend on a particular Nextcloud error-body schema.

1 similar comment
@steipete

steipete commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Maintainer verification complete for exact head 33c26657224b5af89484d33915c165f311b6ca29.

  • Best-fix check: this is the right owner boundary and the leanest fix. collapseErrorSnippet is the one formatter shared by send and reaction failures, and truncateUtf16Safe is already the public plugin-SDK contract for this exact surrogate-boundary invariant. Moving the behavior into the byte reader, adding a Nextcloud-only helper, or post-processing individual errors would broaden or duplicate policy.
  • Test refinement: the regression now compares the complete thrown Error, so a dangling surrogate cannot pass via substring matching.
  • Sanitized AWS green: run run_fff37b03fe84 passed the complete Nextcloud Talk suite: 16 files, 101 tests.
  • Sanitized AWS control red: run run_32cee4fec874 started from the same exact SHA, reintroduced only the former raw slice(0, 200) call in its ephemeral remote worktree, and the focused regression failed as expected: one failed, one passed; the received message showed the split-surrogate replacement character.
  • Exact-head CI: run 29030125468 succeeded with no pending or failing relevant checks.
  • Fresh autoreview: clean, no actionable findings, 0.91 confidence.

No external Nextcloud instance was used. The regression does cross a real local HTTP request/response boundary through sendMessageNextcloudTalk; the changed behavior is the local post-response formatter and does not depend on a particular Nextcloud error-body schema.

@clawsweeper clawsweeper Bot added rating: 🦀 challenger crab Exceptional PR readiness: strong proof, clean patch, and convincing validation. and removed rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Jul 9, 2026
@steipete
steipete merged commit 88acda1 into openclaw:main Jul 9, 2026
136 of 141 checks passed
@steipete

steipete commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merged via squash.

Simon-XYDT pushed a commit to Simon-XYDT/openclaw that referenced this pull request Jul 9, 2026
* fix(nextcloud-talk): keep error snippets UTF-16 safe

* test(nextcloud-talk): assert exact safe error

---------

Co-authored-by: Peter Steinberger <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 10, 2026
* fix(nextcloud-talk): keep error snippets UTF-16 safe

* test(nextcloud-talk): assert exact safe error

---------

Co-authored-by: Peter Steinberger <[email protected]>
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 14, 2026
…ster regression

Adds the mushuiyu886 openclaw#102969/openclaw#102949 proof shape to the existing regression:

- LABEL_TRUNCATION_MODE=baseline swaps the SDK truncateUtf16Safe with raw
  String.prototype.slice via vi.mock, so the same test cases run as
  control-red on the buggy code. Three of the four boundary cases fail
  with 'lone high surrogate detected' (one passes: the no-cut baseline).
- LABEL_PROOF_DUMP=1 writes a [proof] line per case with the actual
  length=, hex_tail= of the parsed display_name= value plus the
  isLoneHighSurrogate verdict. Crabbox can capture the line and the PR
  body can paste it next to the run URL.
- vi.hoisted() lifts the env-var reads so the vi.mock factory (which is
  hoisted to the top of the file) can reference them safely.

No production code change.  No new SDK surface.  No touched files
outside extensions/discord/src/voice/.
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 14, 2026
…gressContext

P1 follow-up: the regression test now drives the production
resolveDiscordVoiceIngressContext end-to-end instead of mocking it.

Before: vi.mock("./ingress.js") returned a stub
{ senderIsOwner: false, speakerLabel: "tester" } with no
extraSystemPrompt. The test still exercised the real
appendDiscordVoiceParticipantContext, but the upstream auth + speaker
identity layer was stubbed.

After: the upstream resolveDiscordVoiceIngressContext is the real
production function. The test passes:
- a real DiscordVoiceSpeakerContextResolver instance with a stub
  client whose fetchMember / fetchUser return emoji-laden display data
- cfg = {} and discordConfig = { groupPolicy: "open" } so
  authorizeDiscordVoiceIngress returns { ok: true, channelConfig: null }
  via the owner-allow-all short-circuit and the default-open group
  policy path
- ownerAllowAll: true so the resolveCommandAuthorizedFromAuthorizers
  authorizer is { configured: true, allowed: true }

What now runs for real (not mocked):
- DiscordVoiceSpeakerContextResolver (class, not a stub): exercises
  resolveIdentity + cache + resolveIsOwner
- authorizeDiscordVoiceIngress: real production auth check
- buildDiscordGroupSystemPrompt: real production prompt builder
- appendDiscordVoiceParticipantContext: real production roster builder

What stays mocked:
- the SDK truncateUtf16Safe (only via LABEL_TRUNCATION_MODE=baseline
  vi.mock, which is the test-only control-red affordance)
- the gateway plugin client (the only way to inject listVoiceChannelStates
  state without a real Discord connection)

P2 follow-up: also add the ephemeral worktree control-red shape (the
mushuiyu886 openclaw#102949 / openclaw#102969 gold standard). The test file itself is
unchanged for P2; the proof is captured by running the test in an
ephemeral worktree with the production code reverted to the buggy raw
slice. See 4.5 of the PR body for the full transcript.

Local verification:
- oxlint --threads=1 on the test file: clean (exit 0)
- vitest in fix mode (default): 4 / 4 pass, hex_tail all clean
- vitest in baseline mode (LABEL_TRUNCATION_MODE=baseline): 3 / 4 fail
  with lone high surrogate detected at the boundary
- vitest in ephemeral worktree with participant-context.ts reverted to
  the original slice(0, 100): 3 / 4 fail with the same lone high
  surrogate; the no-cut baseline still passes
- vitest with LABEL_PROOF_DUMP=1 in either mode: emits the [proof]
  line with length= / hex_tail= / lone_surrogate= / sample= per case

Co-Authored-By: Claude <[email protected]>
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 14, 2026
…ster regression

Adds the mushuiyu886 openclaw#102969/openclaw#102949 proof shape to the existing regression:

- LABEL_TRUNCATION_MODE=baseline swaps the SDK truncateUtf16Safe with raw
  String.prototype.slice via vi.mock, so the same test cases run as
  control-red on the buggy code. Three of the four boundary cases fail
  with 'lone high surrogate detected' (one passes: the no-cut baseline).
- LABEL_PROOF_DUMP=1 writes a [proof] line per case with the actual
  length=, hex_tail= of the parsed display_name= value plus the
  isLoneHighSurrogate verdict. Crabbox can capture the line and the PR
  body can paste it next to the run URL.
- vi.hoisted() lifts the env-var reads so the vi.mock factory (which is
  hoisted to the top of the file) can reference them safely.

No production code change.  No new SDK surface.  No touched files
outside extensions/discord/src/voice/.
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 14, 2026
…gressContext

P1 follow-up: the regression test now drives the production
resolveDiscordVoiceIngressContext end-to-end instead of mocking it.

Before: vi.mock("./ingress.js") returned a stub
{ senderIsOwner: false, speakerLabel: "tester" } with no
extraSystemPrompt. The test still exercised the real
appendDiscordVoiceParticipantContext, but the upstream auth + speaker
identity layer was stubbed.

After: the upstream resolveDiscordVoiceIngressContext is the real
production function. The test passes:
- a real DiscordVoiceSpeakerContextResolver instance with a stub
  client whose fetchMember / fetchUser return emoji-laden display data
- cfg = {} and discordConfig = { groupPolicy: "open" } so
  authorizeDiscordVoiceIngress returns { ok: true, channelConfig: null }
  via the owner-allow-all short-circuit and the default-open group
  policy path
- ownerAllowAll: true so the resolveCommandAuthorizedFromAuthorizers
  authorizer is { configured: true, allowed: true }

What now runs for real (not mocked):
- DiscordVoiceSpeakerContextResolver (class, not a stub): exercises
  resolveIdentity + cache + resolveIsOwner
- authorizeDiscordVoiceIngress: real production auth check
- buildDiscordGroupSystemPrompt: real production prompt builder
- appendDiscordVoiceParticipantContext: real production roster builder

What stays mocked:
- the SDK truncateUtf16Safe (only via LABEL_TRUNCATION_MODE=baseline
  vi.mock, which is the test-only control-red affordance)
- the gateway plugin client (the only way to inject listVoiceChannelStates
  state without a real Discord connection)

P2 follow-up: also add the ephemeral worktree control-red shape (the
mushuiyu886 openclaw#102949 / openclaw#102969 gold standard). The test file itself is
unchanged for P2; the proof is captured by running the test in an
ephemeral worktree with the production code reverted to the buggy raw
slice. See 4.5 of the PR body for the full transcript.

Local verification:
- oxlint --threads=1 on the test file: clean (exit 0)
- vitest in fix mode (default): 4 / 4 pass, hex_tail all clean
- vitest in baseline mode (LABEL_TRUNCATION_MODE=baseline): 3 / 4 fail
  with lone high surrogate detected at the boundary
- vitest in ephemeral worktree with participant-context.ts reverted to
  the original slice(0, 100): 3 / 4 fail with the same lone high
  surrogate; the no-cut baseline still passes
- vitest with LABEL_PROOF_DUMP=1 in either mode: emits the [proof]
  line with length= / hex_tail= / lone_surrogate= / sample= per case

Co-Authored-By: Claude <[email protected]>
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 14, 2026
Round 2 follow-up: slim the regression test and PR body to match the
sibling UTF-16-safe cohort (openclaw#106328, openclaw#102949, openclaw#102969) template.

- Test: 304 -> 127 lines (-58%)
  - Removed LABEL_PROOF_DUMP / LABEL_RUN_LOG env vars + dumpDisplayName helper
  - Extracted renderRosterPrompt(nick) + parseDisplayName(prompt) helpers
  - Trimmed 47-line test-file header to 9 lines
  - Kept LABEL_TRUNCATION_MODE=baseline vi.mock for in-process control-red
  - All 4 emoji-boundary cases preserved
- Body: ~30KB -> 10.5KB (-64%)
  - Dropped Round 1/2/3 rebase history
  - Dropped hex code-unit dump
  - Dropped TODO Crabbox run placeholders
  - Restructured to 4-section template
- Production code unchanged: same 1-line + import + named constant patch.
  Same fix/baseline behavior (4/4 pass in fix mode, 3 fail in baseline).

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: nextcloud-talk Channel integration: nextcloud-talk P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦀 challenger crab Exceptional PR readiness: strong proof, clean patch, and convincing validation. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants