sigit-code 1.7.2__tar.gz → 1.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sigit_code-1.7.2 → sigit_code-1.7.4}/Cargo.lock +2 -2
- {sigit_code-1.7.2 → sigit_code-1.7.4}/Cargo.toml +2 -2
- {sigit_code-1.7.2 → sigit_code-1.7.4}/PKG-INFO +1 -1
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/main.rs +76 -5
- sigit_code-1.7.4/crates/sigit-agent/assets/sign-in-logo.png +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/browser_auth.rs +88 -26
- {sigit_code-1.7.2 → sigit_code-1.7.4}/README.md +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/Cargo.toml +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/README.md +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/account.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/backend.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/chat.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/client_fs.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/client_terminal.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/commands.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/frontmatter.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/harness_markup.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/headless.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/hooks.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/inline_tool_calls.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/instructions.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/mcp.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/models.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/permissions.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/program_status.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/provider.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/repetition.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/session_store.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/settings.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/setup.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/skills.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/subagents.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/tools.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/src/workspace.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_audio.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_client_fs.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_client_terminal.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_concurrent_sessions.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_elicitation.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_embedded_context.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_error_codes.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_harness_markup.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_images.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_logout.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_permissions.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_session_list.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_session_load.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_stop_reasons.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_tool_call_name.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/headless_mode.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/cli/tests/version_flag.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/Cargo.toml +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/README.md +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/auth.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/credentials.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/lib.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/profile.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/src/prompt.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/crates/sigit-agent/tests/embedded.rs +0 -0
- {sigit_code-1.7.2 → sigit_code-1.7.4}/pyproject.toml +0 -0
|
@@ -5945,7 +5945,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
|
5945
5945
|
|
|
5946
5946
|
[[package]]
|
|
5947
5947
|
name = "sigit"
|
|
5948
|
-
version = "1.7.
|
|
5948
|
+
version = "1.7.4"
|
|
5949
5949
|
dependencies = [
|
|
5950
5950
|
"agent-client-protocol",
|
|
5951
5951
|
"anyhow",
|
|
@@ -5976,7 +5976,7 @@ dependencies = [
|
|
|
5976
5976
|
|
|
5977
5977
|
[[package]]
|
|
5978
5978
|
name = "sigit-agent"
|
|
5979
|
-
version = "1.7.
|
|
5979
|
+
version = "1.7.4"
|
|
5980
5980
|
dependencies = [
|
|
5981
5981
|
"agent-client-protocol",
|
|
5982
5982
|
"anyhow",
|
|
@@ -3,7 +3,7 @@ resolver = "3"
|
|
|
3
3
|
members = ["crates/cli", "crates/sigit-*"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "1.7.
|
|
6
|
+
version = "1.7.4"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/getsigit/sigit"
|
|
@@ -16,7 +16,7 @@ authors = ["Seto Elkahfi <seto@sigit.si>", "paydii <193906237+paydii@users.norep
|
|
|
16
16
|
# crates.io, so this resolves from the registry rather than the onde-ed checkout next door.
|
|
17
17
|
ed-acp = { version = "1.3.0", default-features = false }
|
|
18
18
|
ed-acp-testkit = "1.3.0"
|
|
19
|
-
sigit-agent = { version = "1.7.
|
|
19
|
+
sigit-agent = { version = "1.7.4", default-features = false, path = "crates/sigit-agent" }
|
|
20
20
|
|
|
21
21
|
# ACP protocol SDK
|
|
22
22
|
agent-client-protocol = { version = "2.2", features = ["unstable_session_fork"] }
|
|
@@ -2587,7 +2587,7 @@ impl SiGitAgent {
|
|
|
2587
2587
|
cancellation: &PromptCancellation,
|
|
2588
2588
|
) {
|
|
2589
2589
|
if workspace.reacquire().await
|
|
2590
|
-
&& let Err(error) = self.
|
|
2590
|
+
&& let Err(error) = self.reinstall_for_tools(session_id).await
|
|
2591
2591
|
{
|
|
2592
2592
|
log::warn!(
|
|
2593
2593
|
"session({session_id}): could not reinstall mid-turn, cancelling the turn: {}",
|
|
@@ -2598,6 +2598,30 @@ impl SiGitAgent {
|
|
|
2598
2598
|
}
|
|
2599
2599
|
}
|
|
2600
2600
|
|
|
2601
|
+
/// Make `session_id` the live session and check that its directory is still
|
|
2602
|
+
/// there. Tools resolve relative paths against the process directory, and
|
|
2603
|
+
/// `install_session` leaves that alone when the session's `cwd` is gone, so
|
|
2604
|
+
/// a prompt or a tool run on such a session would act in the previous
|
|
2605
|
+
/// session's directory. Load, resume and fork do not call this: they
|
|
2606
|
+
/// succeed for a deleted project so a client can still show its history.
|
|
2607
|
+
async fn reinstall_for_tools(
|
|
2608
|
+
&self,
|
|
2609
|
+
session_id: &SessionId,
|
|
2610
|
+
) -> agent_client_protocol::Result<()> {
|
|
2611
|
+
self.activate_session(session_id).await?;
|
|
2612
|
+
let cwd = self.session_cwd.lock().unwrap().clone();
|
|
2613
|
+
match cwd {
|
|
2614
|
+
Some(cwd) if !cwd.is_dir() => Err(agent_client_protocol::Error::new(
|
|
2615
|
+
ErrorCode::ResourceNotFound.into(),
|
|
2616
|
+
format!(
|
|
2617
|
+
"the working directory of session {session_id} no longer exists: {}",
|
|
2618
|
+
cwd.display()
|
|
2619
|
+
),
|
|
2620
|
+
)),
|
|
2621
|
+
_ => Ok(()),
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2601
2625
|
/// Point the process at `state`'s roots and load its conversation.
|
|
2602
2626
|
///
|
|
2603
2627
|
/// The process follows `cwd` because tool calls may use relative paths; the
|
|
@@ -2619,9 +2643,12 @@ impl SiGitAgent {
|
|
|
2619
2643
|
}
|
|
2620
2644
|
workspace::replace_additional_roots(additional_roots.clone());
|
|
2621
2645
|
mcp::set_session_servers(mcp_servers);
|
|
2622
|
-
if cwd.is_dir()
|
|
2623
|
-
|
|
2624
|
-
|
|
2646
|
+
if !cwd.is_dir() {
|
|
2647
|
+
log::warn!(
|
|
2648
|
+
"session({key}): cwd {} no longer exists, prompts and tools on it will fail",
|
|
2649
|
+
cwd.display()
|
|
2650
|
+
);
|
|
2651
|
+
} else if let Err(err) = std::env::set_current_dir(&cwd) {
|
|
2625
2652
|
log::warn!("could not set cwd to {}: {err}", cwd.display());
|
|
2626
2653
|
}
|
|
2627
2654
|
*self.active_session.lock().unwrap() = Some(key.to_string());
|
|
@@ -3270,7 +3297,7 @@ impl SiGitAgent {
|
|
|
3270
3297
|
if cancellation.cancelled.load(Ordering::Acquire) {
|
|
3271
3298
|
return Ok(PromptResponse::new(StopReason::Cancelled));
|
|
3272
3299
|
}
|
|
3273
|
-
self.
|
|
3300
|
+
self.reinstall_for_tools(&session_id).await?;
|
|
3274
3301
|
if cancellation.cancelled.load(Ordering::Acquire) {
|
|
3275
3302
|
return Ok(PromptResponse::new(StopReason::Cancelled));
|
|
3276
3303
|
}
|
|
@@ -7609,6 +7636,50 @@ mod tests {
|
|
|
7609
7636
|
assert!(agent.active_session.lock().unwrap().is_none());
|
|
7610
7637
|
}
|
|
7611
7638
|
|
|
7639
|
+
#[tokio::test]
|
|
7640
|
+
#[allow(clippy::await_holding_lock)] // the lock guards process-global env for the whole test
|
|
7641
|
+
async fn a_session_whose_directory_is_gone_loads_but_refuses_tools() {
|
|
7642
|
+
let _guard = crate::ENV_TEST_LOCK
|
|
7643
|
+
.lock()
|
|
7644
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
7645
|
+
let agent = SiGitAgent::new(
|
|
7646
|
+
Arc::new(ChatEngine::new()),
|
|
7647
|
+
GgufModelConfig::qwen25_3b(),
|
|
7648
|
+
Arc::new(AtomicBool::new(true)),
|
|
7649
|
+
Arc::new(AtomicBool::new(false)),
|
|
7650
|
+
Arc::new(std::sync::Mutex::new(None)),
|
|
7651
|
+
false,
|
|
7652
|
+
false,
|
|
7653
|
+
);
|
|
7654
|
+
let dir = std::env::temp_dir().join(format!("sigit_gone_cwd_{}", std::process::id()));
|
|
7655
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
7656
|
+
let thread = SessionId::new("gone-cwd-thread");
|
|
7657
|
+
agent
|
|
7658
|
+
.sessions
|
|
7659
|
+
.lock()
|
|
7660
|
+
.unwrap()
|
|
7661
|
+
.insert("gone-cwd-thread".into(), agent.session_state(&dir, &[]));
|
|
7662
|
+
std::fs::remove_dir_all(&dir).unwrap();
|
|
7663
|
+
|
|
7664
|
+
// Load, resume and fork only activate, so they still succeed.
|
|
7665
|
+
agent.activate_session(&thread).await.unwrap();
|
|
7666
|
+
|
|
7667
|
+
// A prompt or a tool run does not.
|
|
7668
|
+
*agent.active_session.lock().unwrap() = None;
|
|
7669
|
+
let error = agent.reinstall_for_tools(&thread).await.unwrap_err();
|
|
7670
|
+
assert_eq!(error.code, ErrorCode::ResourceNotFound);
|
|
7671
|
+
assert!(error.message.contains("no longer exists"));
|
|
7672
|
+
|
|
7673
|
+
let cancellation = PromptCancellation::default();
|
|
7674
|
+
let mut workspace = WorkspaceHold::acquire(&agent.workspace_lock).await;
|
|
7675
|
+
workspace.release();
|
|
7676
|
+
*agent.active_session.lock().unwrap() = None;
|
|
7677
|
+
agent
|
|
7678
|
+
.resume_workspace(&mut workspace, &thread, &cancellation)
|
|
7679
|
+
.await;
|
|
7680
|
+
assert!(cancellation.cancelled.load(Ordering::Acquire));
|
|
7681
|
+
}
|
|
7682
|
+
|
|
7612
7683
|
#[test]
|
|
7613
7684
|
fn session_context_message_lists_every_root_of_a_multi_root_project() {
|
|
7614
7685
|
let primary = PathBuf::from("/tmp/primary");
|
|
Binary file
|
|
@@ -180,11 +180,27 @@ pub async fn complete_loopback(flow: Flow) -> Result<String, String> {
|
|
|
180
180
|
let listener = listener.ok_or_else(|| "this sign-in has no local listener".to_string())?;
|
|
181
181
|
|
|
182
182
|
let expected_state = state.clone();
|
|
183
|
-
let code
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
let (code, mut stream) =
|
|
184
|
+
tokio::task::spawn_blocking(move || wait_for_code(listener, &expected_state))
|
|
185
|
+
.await
|
|
186
|
+
.map_err(|error| format!("sign-in was interrupted: {error}"))??;
|
|
187
|
+
|
|
188
|
+
// The browser is still waiting on its request. Answer it only once the
|
|
189
|
+
// exchange has settled, so the page never reports a sign-in that failed.
|
|
190
|
+
let result = exchange(&code, &verifier, &redirect_uri).await;
|
|
191
|
+
match &result {
|
|
192
|
+
Ok(_) => respond(
|
|
193
|
+
&mut stream,
|
|
194
|
+
"Signed in to siGit Code",
|
|
195
|
+
"You can close this tab.",
|
|
196
|
+
),
|
|
197
|
+
Err(_) => respond(
|
|
198
|
+
&mut stream,
|
|
199
|
+
"Sign-in did not complete",
|
|
200
|
+
"Return to siGit Code and try again.",
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
result
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
/// Redeem a code the person pasted in. Returns the signed-in email.
|
|
@@ -198,8 +214,12 @@ pub async fn complete_paste(flow: &Flow, code: &str) -> Result<String, String> {
|
|
|
198
214
|
|
|
199
215
|
/// Accept connections until one carries our callback. Anything else on the
|
|
200
216
|
/// port (a browser probe, a stray scan) is answered and ignored rather than
|
|
201
|
-
/// ending the wait.
|
|
202
|
-
|
|
217
|
+
/// ending the wait. On success the matching connection is returned unanswered,
|
|
218
|
+
/// for the caller to respond to once the code has been redeemed.
|
|
219
|
+
fn wait_for_code(
|
|
220
|
+
listener: TcpListener,
|
|
221
|
+
expected_state: &str,
|
|
222
|
+
) -> Result<(String, std::net::TcpStream), String> {
|
|
203
223
|
let deadline = std::time::Instant::now() + CALLBACK_TIMEOUT;
|
|
204
224
|
listener
|
|
205
225
|
.set_nonblocking(true)
|
|
@@ -243,7 +263,7 @@ fn wait_for_code(listener: TcpListener, expected_state: &str) -> Result<String,
|
|
|
243
263
|
}
|
|
244
264
|
}
|
|
245
265
|
Err(error) => {
|
|
246
|
-
respond(&mut stream, "
|
|
266
|
+
respond(&mut stream, "Nothing to do here", "");
|
|
247
267
|
log::debug!("could not read browser callback: {error}");
|
|
248
268
|
continue 'connections;
|
|
249
269
|
}
|
|
@@ -255,7 +275,7 @@ fn wait_for_code(listener: TcpListener, expected_state: &str) -> Result<String,
|
|
|
255
275
|
.next()
|
|
256
276
|
.and_then(|line| line.split_whitespace().nth(1))
|
|
257
277
|
else {
|
|
258
|
-
respond(&mut stream, "
|
|
278
|
+
respond(&mut stream, "Nothing to do here", "");
|
|
259
279
|
continue;
|
|
260
280
|
};
|
|
261
281
|
|
|
@@ -273,39 +293,68 @@ fn wait_for_code(listener: TcpListener, expected_state: &str) -> Result<String,
|
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
if let Some(error) = error {
|
|
276
|
-
respond(
|
|
277
|
-
&mut stream,
|
|
278
|
-
"Sign-in was cancelled. You can close this tab.",
|
|
279
|
-
);
|
|
296
|
+
respond(&mut stream, "Sign-in cancelled", "You can close this tab.");
|
|
280
297
|
return Err(format!("sign-in was declined ({error})"));
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
let Some(code) = code else {
|
|
284
|
-
respond(&mut stream, "Nothing to do here
|
|
301
|
+
respond(&mut stream, "Nothing to do here", "");
|
|
285
302
|
continue;
|
|
286
303
|
};
|
|
287
304
|
|
|
288
305
|
// The state proves this callback belongs to the flow we started, not
|
|
289
306
|
// to a link someone else sent to this browser.
|
|
290
307
|
if state.as_deref() != Some(expected_state) {
|
|
291
|
-
respond(
|
|
308
|
+
respond(
|
|
309
|
+
&mut stream,
|
|
310
|
+
"Sign-in didn't match",
|
|
311
|
+
"That sign-in did not come from siGit Code.",
|
|
312
|
+
);
|
|
292
313
|
return Err("the sign-in response did not match this request".to_string());
|
|
293
314
|
}
|
|
294
315
|
|
|
295
|
-
|
|
296
|
-
&mut stream,
|
|
297
|
-
"Signed in to siGit Code. You can close this tab.",
|
|
298
|
-
);
|
|
299
|
-
return Ok(code);
|
|
316
|
+
return Ok((code, stream));
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
320
|
+
/// The 64x64 app icon, inlined into the page so it needs no second request.
|
|
321
|
+
const LOGO_PNG: &[u8] = include_bytes!("../assets/sign-in-logo.png");
|
|
322
|
+
|
|
323
|
+
/// Renders the page the browser lands on. Self-contained: inline CSS and an
|
|
324
|
+
/// inline logo, because the page is served from the loopback address and must
|
|
325
|
+
/// not fetch anything. Title and hint are constants, so nothing is escaped.
|
|
326
|
+
fn page(title: &str, hint: &str) -> String {
|
|
327
|
+
let logo = base64::engine::general_purpose::STANDARD.encode(LOGO_PNG);
|
|
328
|
+
let hint = if hint.is_empty() {
|
|
329
|
+
String::new()
|
|
330
|
+
} else {
|
|
331
|
+
format!("<p>{hint}</p>")
|
|
332
|
+
};
|
|
333
|
+
format!(
|
|
334
|
+
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\
|
|
335
|
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
|
|
336
|
+
<meta name=\"color-scheme\" content=\"dark\"><title>{title}</title><style>\
|
|
337
|
+
:root{{color-scheme:dark}}\
|
|
338
|
+
html,body{{margin:0;height:100%}}\
|
|
339
|
+
body{{display:flex;align-items:center;justify-content:center;background:#15171a;\
|
|
340
|
+
font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif}}\
|
|
341
|
+
main{{display:flex;flex-direction:column;align-items:center;text-align:center;\
|
|
342
|
+
padding:0 24px;transform:translateY(-6%);animation:fade-in 240ms ease-out}}\
|
|
343
|
+
img{{width:40px;height:40px;border-radius:9px;opacity:.8;\
|
|
344
|
+
box-shadow:0 0 0 1px rgb(255 255 255/.06)}}\
|
|
345
|
+
h1{{margin:16px 0 0;font-size:15px;line-height:20px;font-weight:500;\
|
|
346
|
+
letter-spacing:-.01em;color:#f3f4f6}}\
|
|
347
|
+
p{{margin:4px 0 0;font-size:13px;line-height:18px;color:#6b7280}}\
|
|
348
|
+
@keyframes fade-in{{from{{opacity:0}}to{{opacity:1}}}}\
|
|
349
|
+
@media(prefers-reduced-motion:reduce){{main{{animation:none}}}}\
|
|
350
|
+
</style></head><body><main>\
|
|
351
|
+
<img src=\"data:image/png;base64,{logo}\" alt=\"\" width=\"40\" height=\"40\">\
|
|
352
|
+
<h1>{title}</h1>{hint}</main></body></html>"
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn respond(stream: &mut std::net::TcpStream, title: &str, hint: &str) {
|
|
357
|
+
let body = page(title, hint);
|
|
309
358
|
let response = format!(
|
|
310
359
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
311
360
|
body.len(),
|
|
@@ -464,6 +513,19 @@ fn percent_decode(value: &str) -> String {
|
|
|
464
513
|
mod tests {
|
|
465
514
|
use super::*;
|
|
466
515
|
|
|
516
|
+
#[test]
|
|
517
|
+
fn page_carries_the_title_hint_and_an_inline_logo() {
|
|
518
|
+
let body = page("Signed in to siGit Code", "You can close this tab.");
|
|
519
|
+
assert!(body.contains("<h1>Signed in to siGit Code</h1>"));
|
|
520
|
+
assert!(body.contains("<p>You can close this tab.</p>"));
|
|
521
|
+
assert!(body.contains("data:image/png;base64,"));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn page_omits_the_hint_when_empty() {
|
|
526
|
+
assert!(!page("Nothing to do here", "").contains("<p>"));
|
|
527
|
+
}
|
|
528
|
+
|
|
467
529
|
#[test]
|
|
468
530
|
fn code_challenge_matches_rfc_7636_example() {
|
|
469
531
|
// RFC 7636 Appendix B.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|