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
3 changes: 3 additions & 0 deletions .clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ doc-valid-idents = [
allow-unwrap-in-tests = true
allow-print-in-tests = true
allowed-duplicate-crates = [
# `ring` uses 0.2; `rand_core` and event IDs use 0.3. Remove this once the
# transitive dependencies converge on one version (T-0jrnyy1).
"getrandom",
# `thiserror-impl` 2.0.20 parses with `syn` 3 while the rest of the
# proc-macro ecosystem is still on 2, so the graph carries both. Nothing in
# the workspace can collapse it: we pin `syn` 2, and moving to 3 would only
Expand Down
5 changes: 5 additions & 0 deletions .config/supply-chain/audits.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ who = "Jean Mertz <git@jeanmertz.com>"
criteria = "safe-to-deploy"
delta = "0.37.0 -> 0.38.0"

[[audits.rustls]]
who = "Jean Mertz <git@jeanmertz.com>"
criteria = "safe-to-deploy"
delta = "0.23.35 -> 0.23.37"

[[audits.rustls-webpki]]
who = "Jean Mertz <git@jeanmertz.com>"
criteria = "safe-to-deploy"
Expand Down
16 changes: 16 additions & 0 deletions .config/supply-chain/imports.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1599,12 +1599,28 @@ who = "Alex Crichton <alex@alexcrichton.com>"
criteria = "safe-to-deploy"
delta = "0.1.21 -> 0.1.24"

[[audits.bytecode-alliance.audits.rustls]]
who = "Alex Crichton <alex@alexcrichton.com>"
criteria = "safe-to-deploy"
delta = "0.23.37 -> 0.23.45"
notes = """
A relatively large update, but no new `unsafe` and nothing awry here. Lots of
protocol/etc updates which I'm not personally an expert within but the rustls
maintainers are relatively well trusted as well.
"""

[[audits.bytecode-alliance.audits.rustls-webpki]]
who = "Alex Crichton <alex@alexcrichton.com>"
criteria = "safe-to-deploy"
delta = "0.103.12 -> 0.103.13"
notes = "Minor fixes for the bug being fixed in this release, nothing awry."

[[audits.bytecode-alliance.audits.rustls-webpki]]
who = "Alex Crichton <alex@alexcrichton.com>"
criteria = "safe-to-deploy"
delta = "0.103.13 -> 0.103.15"
notes = "Minor updates and feature shufflings."

[[audits.bytecode-alliance.audits.sha1]]
who = "Andrew Brown <andrew.brown@intel.com>"
criteria = "safe-to-deploy"
Expand Down
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ eventsource-stream = { version = "0.2", default-features = false }
fancy-regex = { version = "0.19", default-features = false }
futures = { version = "0.3", default-features = false }
gemini_client_rs = { git = "https://github.com/JeanMertz/gemini-client", default-features = false } # <https://github.com/Adriftdev/gemini-client/pull/16>
getrandom = { version = "0.3", default-features = false }
gimli = { version = "0.33" }
glob = { version = "0.3", default-features = false }
grep-matcher = { version = "0.1", default-features = false }
Expand Down
4 changes: 4 additions & 0 deletions crates/jp_cli/src/cmd/conversation/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ pub(crate) struct Edit {
no_title: bool,

/// Open `events.json` in `$EDITOR`.
///
/// Each entry carries an `event_id` identifying it.
/// Keep it when you edit an entry's content, and drop it when you add one:
/// a missing, empty, or duplicated ID is replaced on the next load.
#[arg(long, short = 'e', group = "file", conflicts_with = "property")]
events: bool,

Expand Down
41 changes: 39 additions & 2 deletions crates/jp_cli/src/cmd/conversation/fork_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use jp_conversation::{
use jp_printer::{OutputFormat, Printer};
use jp_storage::backend::{FsStorageBackend, Projection};
use jp_workspace::Workspace;
use serde_json::Value;
use tokio::runtime::Runtime;

use super::*;
Expand All @@ -23,6 +24,42 @@ use crate::{
cmd::{compact_flag::CompactFlag, conversation_id::PositionalIds},
};

/// Assert a fork reproduces its source, entry IDs included.
///
/// A fork copies the source's entries rather than rebuilding them, so each one
/// keeps its ID and a reference into the source resolves against the fork.
///
/// The leading `TurnStart` is the exception, and is compared by payload alone:
/// neither stream stored one, so `sanitize` synthesized a separate entry on
/// each side.
/// Two independently created entries are two entries, and their IDs are meant
/// to differ.
fn assert_forked_stream_matches(source: &ConversationStream, fork: &ConversationStream) {
let stored_ids = |stream: &ConversationStream| -> Vec<String> {
stream
.iter()
.filter(|event| !event.is_turn_start())
.map(|event| event.event_id.to_string())
.collect()
};
assert_eq!(
stored_ids(source),
stored_ids(fork),
"a forked entry must keep the ID it had in the source"
);

let payloads = |stream: &ConversationStream| -> Vec<Value> {
let (_, mut events) = stream.to_parts().unwrap();
for event in &mut events {
event.as_object_mut().unwrap().shift_remove("event_id");
}
events
};
assert_eq!(payloads(source), payloads(fork));
assert_eq!(source.base_config(), fork.base_config());
assert_eq!(source.created_at, fork.created_at);
}

/// Parse a [`TurnSelection`] from the flags a user would pass to `jp c fork`.
///
/// Going through clap keeps these cases pinned to the real flag surface rather
Expand Down Expand Up @@ -173,7 +210,7 @@ fn test_conversation_fork() {
assert!(convs[0].0.timestamp() < convs[1].0.timestamp());
assert_eq!(convs[0].1, convs[1].1);
convs[0].2.sanitize();
assert_eq!(convs[0].2, convs[1].2);
assert_forked_stream_matches(&convs[0].2, &convs[1].2);
},
}),
("no turns keeps config but drops every turn", TestCase {
Expand Down Expand Up @@ -282,7 +319,7 @@ fn test_conversation_fork() {
assert!(convs[0].0.timestamp() < convs[1].0.timestamp());
assert_eq!(convs[0].1, convs[1].1);
convs[0].2.sanitize();
assert_eq!(convs[0].2, convs[1].2);
assert_forked_stream_matches(&convs[0].2, &convs[1].2);
},
}),
("with from", TestCase {
Expand Down
4 changes: 2 additions & 2 deletions crates/jp_cli/src/cmd/conversation/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ impl Grep {

// `iter_events_by_turn` rather than `iter_turns`: the latter resolves and
// clones a `PartialAppConfig` per event, which grep never reads.
for (index, event) in events.iter_events_by_turn() {
for event in events.iter_events_by_turn() {
if budget.is_exhausted() {
return group;
}
Expand All @@ -434,7 +434,7 @@ impl Grep {
&mut group.hits,
// 1-based to match the `--turn` selector and the headers `print`
// renders.
Some(index + 1),
Some(event.turn + 1),
scope,
Some(event.timestamp),
&line_refs,
Expand Down
67 changes: 67 additions & 0 deletions crates/jp_cli/src/cmd/plugin/dispatch_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,73 @@ fn draft(response: HostToPlugin) -> jp_plugin::message::DraftResponse {
}
}

/// Unwrap an events response, or say what came back instead.
fn events(response: HostToPlugin) -> jp_plugin::message::EventsResponse {
match response {
HostToPlugin::Events(events) => events,
other => panic!("expected an events response, got {other:?}"),
}
}

/// Every entry a plugin reads carries the `event_id` its stored form has.
///
/// This is the one user-visible consequence of stable event IDs: a plugin can
/// name an entry it read and have that name still mean the same entry later.
/// Asserted for every entry rather than the first, because only conversation
/// events reach the iteration views — a compaction is addressable here and
/// nowhere else.
#[test]
fn read_events_gives_a_plugin_each_entrys_id() {
let (ws, id, _tmp) = workspace_with_conversation();
let handle = ws.acquire_conversation(&id).unwrap();
let stored_ids = ws.test_lock(handle).as_mut().update_events(|stream| {
stream.start_turn("question");
stream.add_compaction(jp_conversation::Compaction::new(0, 0));
stream
.to_parts()
.unwrap()
.1
.iter()
.map(|event| event["event_id"].as_str().unwrap().to_owned())
.collect::<Vec<_>>()
});

let read = events(handle_read_events(&ws, &wire_id(id), None));

assert_eq!(read.conversation, wire_id(id));
let read_ids: Vec<_> = read
.data
.iter()
.map(|event| event["event_id"].as_str().unwrap_or_default().to_owned())
.collect();
assert_eq!(read_ids, stored_ids);
assert!(read_ids.iter().all(|id| !id.is_empty()));
}

/// Decoding content for the plugin must not disturb the entry's identity.
#[test]
fn read_events_decodes_content_without_touching_the_id() {
let (ws, id, _tmp) = workspace_with_conversation();
let handle = ws.acquire_conversation(&id).unwrap();
ws.test_lock(handle)
.as_mut()
.update_events(|stream| stream.start_turn("a question"));

let read = events(handle_read_events(&ws, &wire_id(id), None));

let request = read
.data
.iter()
.find(|event| event["type"] == "chat_request")
.expect("the turn's chat request");
assert_eq!(request["content"], "a question");
assert!(
request["event_id"]
.as_str()
.is_some_and(|id| !id.is_empty())
);
}

/// A conversation with no draft reads back empty rather than as an error: most
/// conversations never have one.
#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/jp_conversation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jp_id = { workspace = true }

base64 = { workspace = true, features = ["std"] }
chrono = { workspace = true }
getrandom = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
jp_label = { workspace = true }
quick-xml = { workspace = true, features = ["serialize"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/jp_conversation/src/compaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//!
//! See [RFD 064].
//!
//! [RFD 064]: https://github.com/dcdpr/jp/blob/main/docs/rfd/064-non-destructive-conversation-compaction.md
//! [RFD 064]: https://jp.computer/rfd/064

use chrono::{DateTime, Utc};
pub use jp_config::types::{byte_size::ByteSize, policy_spec::PolicySpec};
Expand Down
8 changes: 8 additions & 0 deletions crates/jp_conversation/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ pub enum Error {
#[error("Invalid ID format: {0}")]
InvalidIdFormat(String),

/// An event ID was empty.
///
/// A stream entry's identity cannot be the empty string: an absent ID is
/// assigned one at load, so an empty one reaching this far is a caller
/// error rather than a legacy file.
#[error("Event ID must not be empty.")]
EmptyEventId,

/// Invalid ID.
#[error("Invalid ID: {0}")]
Id(#[from] jp_id::Error),
Expand Down
Loading
Loading