Skip to content

Commit 0225d5b

Browse files
sanityclaude
andauthored
fix: send UpdateResponse to clients when update results in no state change (#1734)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 893058a commit 0225d5b

File tree

12 files changed

+650
-48
lines changed

12 files changed

+650
-48
lines changed

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ members = [
55
"apps/freenet-ping/app",
66
"apps/freenet-ping/types",
77
"apps/freenet-ping/contracts/ping",
8-
"tests/test-contract-integration"
8+
"tests/test-contract-integration",
9+
"tests/test-contract-update-nochange"
910
]
1011

1112
[workspace.dependencies]

PRE_COMMIT_HOOK_GUIDE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Pre-Commit Hook with Claude Test Detection
2+
3+
## Overview
4+
5+
The pre-commit hook now includes Claude-powered detection of disabled tests to prevent accidentally committing code with disabled tests.
6+
7+
## How It Works
8+
9+
The hook performs the following checks:
10+
1. **Rust formatting** - Ensures code follows standard formatting
11+
2. **Clippy linting** - Catches common Rust mistakes and anti-patterns
12+
3. **TODO-MUST-FIX detection** - Blocks commits with TODO-MUST-FIX comments
13+
4. **Disabled test detection** - Uses Claude to analyze the diff and detect disabled tests
14+
15+
## Disabled Test Detection
16+
17+
Claude will detect various patterns indicating disabled tests:
18+
- Rust: `#[ignore]`, `#[ignore = "reason"]`
19+
- JavaScript/TypeScript: `it.skip()`, `describe.skip()`, `test.skip()`
20+
- Python: `@pytest.mark.skip`, `@unittest.skip`
21+
- Commented out test functions
22+
- Any other language-specific test disabling patterns
23+
24+
When disabled tests are detected, the hook will:
25+
1. Block the commit
26+
2. Show exactly where the disabled tests were found
27+
3. Provide guidance on how to proceed
28+
29+
## Example Output
30+
31+
```bash
32+
Checking for disabled tests with Claude...
33+
✗ Claude detected disabled tests in the commit
34+
Disabled tests found at:
35+
test_example.rs:6-9 - test marked with #[ignore] attribute
36+
Tests should not be disabled in commits. If a test must be temporarily disabled:
37+
1. Add a TODO-MUST-FIX comment explaining why
38+
2. Fix the underlying issue before committing
39+
3. Or exclude the test changes from this commit
40+
```
41+
42+
## Handling Disabled Tests
43+
44+
If you need to temporarily disable a test:
45+
46+
1. **Add TODO-MUST-FIX comment**: This will also block the commit, forcing you to address it
47+
```rust
48+
// TODO-MUST-FIX: This test hangs during startup
49+
#[ignore = "See TODO-MUST-FIX above"]
50+
fn test_broken() { }
51+
```
52+
53+
2. **Fix the test**: The preferred solution is to fix the underlying issue
54+
55+
3. **Exclude from commit**: Use `git add -p` to selectively stage changes, excluding the disabled test
56+
57+
## Requirements
58+
59+
- Claude CLI must be installed at `~/.claude/local/claude`
60+
- The hook will gracefully skip Claude checks if the CLI is not available
61+
62+
## Troubleshooting
63+
64+
- If Claude checks are failing unexpectedly, check that the Claude CLI is working:
65+
```bash
66+
~/.claude/local/claude --help
67+
```
68+
- The hook won't fail the commit if Claude itself has an error (only if disabled tests are found)

crates/core/src/client_events/mod.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -434,26 +434,28 @@ async fn process_open_request(
434434
?data,
435435
"Starting update op",
436436
);
437-
let new_state = match op_manager
437+
let update_response = op_manager
438438
.notify_contract_handler(ContractHandlerEvent::UpdateQuery {
439439
key,
440440
data,
441441
related_contracts: related_contracts.clone(),
442442
})
443-
.await
444-
{
445-
Ok(ContractHandlerEvent::UpdateResponse {
443+
.await?;
444+
445+
let new_state = match update_response {
446+
ContractHandlerEvent::UpdateResponse {
446447
new_value: Ok(new_val),
447-
}) => Ok(new_val),
448-
Ok(ContractHandlerEvent::UpdateResponse {
448+
} => Ok(new_val),
449+
ContractHandlerEvent::UpdateResponse {
449450
new_value: Err(err),
450-
}) => Err(OpError::from(err)),
451-
Ok(ContractHandlerEvent::UpdateNoChange { key }) => {
452-
tracing::debug!(%key, "update with no change, do not start op");
453-
return Ok(None);
451+
} => Err(OpError::from(err)),
452+
ContractHandlerEvent::UpdateNoChange { key } => {
453+
// This should not happen anymore since we now return UpdateResponse
454+
// from the contract handler even for NoChange cases
455+
tracing::warn!(%key, "Unexpected UpdateNoChange event - this should have been converted to UpdateResponse");
456+
return Err(OpError::UnexpectedOpState.into());
454457
}
455-
Err(err) => Err(err.into()),
456-
Ok(_) => Err(OpError::UnexpectedOpState),
458+
_ => return Err(OpError::UnexpectedOpState.into()),
457459
}
458460
.inspect_err(|err| tracing::error!(%key, "update query failed: {}", err))?;
459461

crates/core/src/client_events/websocket.rs

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,25 @@ async fn process_host_response(
552552
HostResponse::Ok => "HostResponse::Ok",
553553
_ => "Unknown",
554554
};
555-
tracing::debug!(response = %res, response_type, cli_id = %id, "sending response");
555+
556+
// Enhanced logging for UPDATE responses
557+
match &res {
558+
HostResponse::ContractResponse(ContractResponse::UpdateResponse {
559+
key,
560+
summary,
561+
}) => {
562+
tracing::debug!(
563+
"Processing UpdateResponse for WebSocket delivery - client: {}, key: {}, summary length: {}",
564+
id,
565+
key,
566+
summary.size()
567+
);
568+
}
569+
_ => {
570+
tracing::debug!(response = %res, response_type, cli_id = %id, "sending response");
571+
}
572+
}
573+
556574
match res {
557575
HostResponse::ContractResponse(ContractResponse::GetResponse {
558576
key,
@@ -572,14 +590,64 @@ async fn process_host_response(
572590
Err(err)
573591
}
574592
};
593+
// Log when UPDATE response is about to be sent over WebSocket
594+
let is_update_response = match &result {
595+
Ok(HostResponse::ContractResponse(ContractResponse::UpdateResponse {
596+
key,
597+
..
598+
})) => {
599+
tracing::debug!(
600+
"About to serialize UpdateResponse for WebSocket delivery - client: {}, key: {}",
601+
client_id,
602+
key
603+
);
604+
Some(*key)
605+
}
606+
_ => None,
607+
};
608+
575609
let serialized_res = match encoding_protoc {
576610
EncodingProtocol::Flatbuffers => match result {
577611
Ok(res) => res.into_fbs_bytes()?,
578612
Err(err) => err.into_fbs_bytes()?,
579613
},
580614
EncodingProtocol::Native => bincode::serialize(&result)?,
581615
};
582-
tx.send(Message::Binary(serialized_res)).await?;
616+
617+
// Log serialization completion for UPDATE responses
618+
if let Some(key) = is_update_response {
619+
tracing::debug!(
620+
"Serialized UpdateResponse for WebSocket delivery - client: {}, key: {}, size: {} bytes",
621+
client_id,
622+
key,
623+
serialized_res.len()
624+
);
625+
}
626+
627+
let send_result = tx.send(Message::Binary(serialized_res)).await;
628+
629+
// Log WebSocket send result for UPDATE responses
630+
if let Some(key) = is_update_response {
631+
match &send_result {
632+
Ok(()) => {
633+
tracing::debug!(
634+
"Successfully sent UpdateResponse over WebSocket to client {} for key {}",
635+
client_id,
636+
key
637+
);
638+
}
639+
Err(err) => {
640+
tracing::error!(
641+
"Failed to send UpdateResponse over WebSocket to client {} for key {}: {:?}",
642+
client_id,
643+
key,
644+
err
645+
);
646+
}
647+
}
648+
}
649+
650+
send_result?;
583651
Ok(None)
584652
}
585653
Some(HostCallbackResult::SubscriptionChannel { key, id, callback }) => {
@@ -627,20 +695,88 @@ impl ClientEventsProxy for WebSocketProxy {
627695
result: Result<HostResponse, ClientError>,
628696
) -> BoxFuture<Result<(), ClientError>> {
629697
async move {
698+
// Log UPDATE responses specifically
699+
match &result {
700+
Ok(HostResponse::ContractResponse(freenet_stdlib::client_api::ContractResponse::UpdateResponse { key, summary })) => {
701+
tracing::debug!(
702+
"WebSocket send() called with UpdateResponse for client {} - key: {}, summary length: {}",
703+
id,
704+
key,
705+
summary.size()
706+
);
707+
}
708+
Ok(other_response) => {
709+
tracing::debug!("WebSocket send() called with response for client {}: {:?}", id, other_response);
710+
}
711+
Err(error) => {
712+
tracing::debug!("WebSocket send() called with error for client {}: {:?}", id, error);
713+
}
714+
}
715+
630716
if let Some(ch) = self.response_channels.remove(&id) {
717+
// Log success/failure of sending UPDATE responses
718+
if let Ok(HostResponse::ContractResponse(freenet_stdlib::client_api::ContractResponse::UpdateResponse { key, .. })) = &result {
719+
tracing::debug!(
720+
"Found WebSocket channel for client {}, sending UpdateResponse for key {}",
721+
id,
722+
key
723+
);
724+
}
725+
726+
// Check if this is an UPDATE response and extract key before moving result
727+
let update_key = match &result {
728+
Ok(HostResponse::ContractResponse(freenet_stdlib::client_api::ContractResponse::UpdateResponse { key, .. })) => Some(*key),
729+
_ => None
730+
};
731+
631732
let should_rm = result
632733
.as_ref()
633734
.map_err(|err| matches!(err.kind(), ErrorKind::Disconnect))
634735
.err()
635736
.unwrap_or(false);
636-
if ch.send(HostCallbackResult::Result { id, result }).is_ok() && !should_rm {
737+
738+
let send_result = ch.send(HostCallbackResult::Result { id, result });
739+
740+
// Log UPDATE response send result
741+
if let Some(key) = update_key {
742+
match send_result.is_ok() {
743+
true => {
744+
tracing::debug!(
745+
"Successfully sent UpdateResponse to client {} for key {}",
746+
id,
747+
key
748+
);
749+
}
750+
false => {
751+
tracing::error!(
752+
"Failed to send UpdateResponse to client {} for key {} - channel send failed",
753+
id,
754+
key
755+
);
756+
}
757+
}
758+
}
759+
760+
if send_result.is_ok() && !should_rm {
637761
// still alive connection, keep it
638762
self.response_channels.insert(id, ch);
639763
} else {
640764
tracing::info!("dropped connection to client #{id}");
641765
}
642766
} else {
643-
tracing::warn!("client: {id} not found");
767+
// Log when client is not found for UPDATE responses
768+
match &result {
769+
Ok(HostResponse::ContractResponse(freenet_stdlib::client_api::ContractResponse::UpdateResponse { key, .. })) => {
770+
tracing::error!(
771+
"Client {} not found in WebSocket response channels when trying to send UpdateResponse for key {}",
772+
id,
773+
key
774+
);
775+
}
776+
_ => {
777+
tracing::warn!("client: {id} not found");
778+
}
779+
}
644780
}
645781
Ok(())
646782
}

crates/core/src/contract/mod.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,29 @@ where
144144
.await;
145145

146146
let event_result = match update_result {
147-
Ok(UpsertResult::NoChange) => ContractHandlerEvent::UpdateNoChange { key },
147+
Ok(UpsertResult::NoChange) => {
148+
tracing::info!(%key, "UPDATE resulted in NoChange, fetching current state to return UpdateResponse");
149+
// When there's no change, we still need to return the current state
150+
// so the client gets a proper response
151+
match contract_handler.executor().fetch_contract(key, false).await {
152+
Ok((Some(current_state), _)) => {
153+
tracing::info!(%key, "Successfully fetched current state for NoChange update");
154+
ContractHandlerEvent::UpdateResponse {
155+
new_value: Ok(current_state),
156+
}
157+
}
158+
Ok((None, _)) => {
159+
tracing::warn!(%key, "No state found when fetching for NoChange update");
160+
// Fallback to the old behavior if we can't fetch the state
161+
ContractHandlerEvent::UpdateNoChange { key }
162+
}
163+
Err(err) => {
164+
tracing::error!(%key, %err, "Error fetching state for NoChange update");
165+
// Fallback to the old behavior if we can't fetch the state
166+
ContractHandlerEvent::UpdateNoChange { key }
167+
}
168+
}
169+
}
148170
Ok(UpsertResult::Updated(state)) => ContractHandlerEvent::UpdateResponse {
149171
new_value: Ok(state),
150172
},

0 commit comments

Comments
 (0)