Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# How to perform migrations
## How do migrations work?
A sqlite database is one single file on the user's computer, and instead of being a separate process, it's a set of C functions
that are bundled in our app. When the warp app starts, we upgrade the schema to the latest version in a transaction.
Since we don't control the machine, we need to be super careful about the migrations that we ship.
TODO: C.I. and remediation of failed migrations
## Step 1: One-time setup
Make sure you have run `/script/bootstrap` at least once before. That installs our fork of
`diesel_cli`. Our fork is an old version that [bundles](https://github.com/warpdotdev/diesel/blob/b2c58897c39c519a946314bd5b63765d3af56204/diesel_cli/Cargo.toml#L54)
SQLite in with the `diesel_cli`. We use these version of Diesel and SQLite instead of relying on
the versions on our machines. So do not follow the official Diesel CLI installation instructions.
Additionally, the `sqlite3` binary is useful in development. The one on your Macbook is ok, but
likely will be missing some basic language features. You can grab the latest binary from the
official website https://www.sqlite.org/download.html. Note that we use SQL language features that
are not available on old versions of sqlite. Note that you'll have to approve it to be run in your mac system preferences.
## Step 2: Write the migration
```
diesel migration generate <descriptive name of your migration>
```
This will create a new folder with an up.sql and down.sql.
## Step 3: Run the migration + generate the schema
```
cd <warp-internal repo>
diesel migration run --database-url="/Users/$USER/Library/Application Support/dev.warp.Warp-Local/warp.sqlite"
```
This will run the migration on the same warp that runs when you run the app locally. This automatically generates or updates the `app/src/persistence/schema.rs`. We do not make manual edits to `schema.rs`.
You can also print the schema from a database that already has the migration with:
```
diesel print-schema --database-url="/Users/$USER/Library/Application Support/dev.warp.Warp-Local/warp.sqlite"
```
## Reverting/redo-ing migrations
As you are writing features and changing branches, you'll want to undo migrations to fix your database and make it compatible with older code. Redo-ing can also be helpful as you are iterating on your schema.
```
diesel migration revert --database-url="/Users/$USER/Library/Application Support/dev.warp.Warp-Local/warp.sqlite"
diesel migration redo --database-url="/Users/$USER/Library/Application Support/dev.warp.Warp-Local/warp.sqlite"
```
# Schema style
- We use `id` for integer primary keys. If there's something more special about the primary key, consider a more descriptive name.
- Plural table names, singular for structs in the rust model code.
- If there's a table `foos` and a table `bars`, and `bars` has a column with a foreign key that references `foos.id`, name it `foo_id`.
# The `schema.patch` file
The `app/src/persistence/schema.patch` is manually updated by us. This file lets us make manual
changes on top of the auto-generated `schema.rs` file produced by `diesel_cli`.
To create the `schema.patch` file, we:
1. Run the diesel migrations
1. Manually edit `schema.rs`
1. Run `git diff -U6 > app/src/persistence/schema.patch`.
You can read more about this patch file in the official [Diesel documentation](https://diesel.rs/guides/configuring-diesel-cli.html#the-patch_file-field).
+214
View File
@@ -0,0 +1,214 @@
use diesel::associations::HasTable;
use diesel::{prelude::*, result::Error, SqliteConnection};
use prost::Message;
use std::collections::{HashMap, HashSet};
use warp_multi_agent_api as api;
use super::model::{AgentConversation, AgentConversationData};
use crate::persistence::model::{AgentConversationRecord, AgentTaskRecord};
use crate::persistence::schema::{self, agent_conversations, agent_tasks};
#[derive(Debug, Insertable, AsChangeset)]
#[diesel(table_name = agent_conversations)]
struct NewAgentConversation {
conversation_id: String,
conversation_data: String,
}
#[derive(Debug, Insertable, AsChangeset)]
#[diesel(table_name = agent_tasks)]
struct NewAgentTask {
conversation_id: String,
task_id: String,
task: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub(super) enum UpsertConversationError {
#[error("Failed to serialize conversation data: {0:?}")]
Serialization(#[from] serde_json::Error),
#[error("Failed to upsert conversation to sqlite: {0:?}")]
DB(#[from] diesel::result::Error),
}
pub(super) fn upsert_agent_conversation<'a>(
conn: &mut SqliteConnection,
conversation_id_param: &str,
tasks: impl IntoIterator<Item = &'a api::Task>,
conversation_data_param: AgentConversationData,
) -> Result<(), UpsertConversationError> {
use diesel::ExpressionMethods;
use diesel::QueryDsl;
use schema::agent_conversations::dsl::*;
use schema::agent_tasks::dsl as tasks_dsl;
const MAX_PERSISTED_CONVERSATION_COUNT: i64 = 100;
let serialized_conversation_data = serde_json::to_string(&conversation_data_param)?;
conn.transaction::<_, Error, _>(|conn| {
// Upsert the conversation level metadata
let new_conversation = NewAgentConversation {
conversation_id: conversation_id_param.to_owned(),
conversation_data: serialized_conversation_data,
};
diesel::insert_into(agent_conversations::table())
.values(&new_conversation)
.on_conflict(conversation_id)
.do_update()
.set(&new_conversation)
.execute(conn)?;
// Upsert each task
for task in tasks {
let task_binary = task.encode_to_vec();
let new_task = NewAgentTask {
conversation_id: conversation_id_param.to_owned(),
task_id: task.id.clone(),
task: task_binary,
};
if let Err(e) = diesel::insert_into(agent_tasks::table)
.values(&new_task)
.on_conflict(tasks_dsl::task_id)
.do_update()
.set(&new_task)
.execute(conn)
{
log::warn!("Failed to upsert task {e:?}");
return Err(e);
}
}
// Prune old conversations if we exceed MAX_PERSISTED_CONVERSATION_COUNT conversations
let conversation_count: i64 = agent_conversations::table().count().get_result(conn)?;
if conversation_count > MAX_PERSISTED_CONVERSATION_COUNT {
// Remove the oldest conversations, keeping only the most recent MAX_PERSISTED_CONVERSATION_COUNT
let conversations_to_remove: Vec<String> = agent_conversations::table()
.order(last_modified_at.asc())
.limit(conversation_count - MAX_PERSISTED_CONVERSATION_COUNT)
.select(conversation_id)
.load(conn)?;
delete_agent_conversations(conn, conversations_to_remove)?;
}
Ok(())
})?;
Ok(())
}
pub(super) fn read_agent_conversations(
conn: &mut SqliteConnection,
) -> Result<Vec<AgentConversation>, diesel::result::Error> {
use schema::agent_conversations::dsl::*;
let mut conversations_by_id = HashMap::<String, AgentConversation>::from_iter(
agent_conversations
.select(AgentConversationRecord::as_select())
.load(conn)?
.into_iter()
.map(|conversation| {
(
conversation.conversation_id.clone(),
AgentConversation {
conversation,
tasks: vec![],
},
)
}),
);
let task_records: Vec<AgentTaskRecord> = agent_tasks::table
.select(AgentTaskRecord::as_select())
.load(conn)?;
let mut invalid_conversation_ids = HashSet::new();
for task_record in task_records {
if let Some(conversation) = conversations_by_id.get_mut(&task_record.conversation_id) {
match api::Task::decode(&task_record.task[..]) {
Ok(api_task) => {
conversation.tasks.push(api_task);
}
Err(e) => {
log::error!("Failed to decode task protobuf: {e}");
invalid_conversation_ids
.insert(conversation.conversation.conversation_id.clone());
}
}
}
}
conversations_by_id.retain(|c_id, _| !invalid_conversation_ids.contains(c_id));
Ok(conversations_by_id.into_values().collect())
}
/// Read a single agent conversation by its ID, including decoded tasks.
pub(crate) fn read_agent_conversation_by_id(
conn: &mut SqliteConnection,
conversation_id_str: &str,
) -> Result<Option<AgentConversation>, diesel::result::Error> {
use schema::agent_conversations::dsl as convo_dsl;
use schema::agent_tasks::dsl as tasks_dsl;
let maybe_record: Option<AgentConversationRecord> = convo_dsl::agent_conversations
.filter(convo_dsl::conversation_id.eq(conversation_id_str.to_owned()))
.select(AgentConversationRecord::as_select())
.first(conn)
.optional()?;
let Some(conversation_record) = maybe_record else {
return Ok(None);
};
let task_records: Vec<AgentTaskRecord> = schema::agent_tasks::table
.filter(tasks_dsl::conversation_id.eq(conversation_id_str))
.select(AgentTaskRecord::as_select())
.load(conn)?;
let mut decoded_tasks = Vec::new();
for task_record in task_records.into_iter() {
match api::Task::decode(&task_record.task[..]) {
Ok(task) => decoded_tasks.push(task),
Err(e) => {
log::error!("Failed to decode task protobuf: {e}");
}
}
}
Ok(Some(AgentConversation {
conversation: conversation_record,
tasks: decoded_tasks,
}))
}
pub(super) fn delete_agent_conversations(
conn: &mut SqliteConnection,
conversation_ids: Vec<String>,
) -> Result<(), diesel::result::Error> {
use diesel::ExpressionMethods;
use diesel::QueryDsl;
use schema::agent_conversations::dsl::*;
use schema::agent_tasks::dsl as tasks_dsl;
conn.transaction::<_, Error, _>(|conn| {
// Delete tasks for these conversations first (due to foreign key constraint)
diesel::delete(
agent_tasks::table.filter(tasks_dsl::conversation_id.eq_any(&conversation_ids)),
)
.execute(conn)?;
// Delete the conversations themselves
diesel::delete(
agent_conversations::table().filter(conversation_id.eq_any(&conversation_ids)),
)
.execute(conn)?;
Ok(())
})?;
Ok(())
}
+292
View File
@@ -0,0 +1,292 @@
//! Manages how we write to and read from our SQLite database for our AI features.
use std::{collections::HashMap, sync::Arc};
use chrono::{Local, NaiveDateTime, TimeZone};
use diesel::{prelude::*, result::Error, sqlite::SqliteConnection};
use itertools::Itertools;
use crate::ai::blocklist::{PersistedAIInput, SerializedBlockListItem};
use crate::terminal::model::block::{SerializedAgentViewVisibility, SerializedBlock};
use crate::{app_state::PaneUuid, persistence::schema::ai_queries};
use super::model::Block;
use super::{model, schema};
const MAX_TERMINAL_BLOCKS_TO_PERSIST_PER_SESSION: i64 = 100;
type PersistedBlocks = HashMap<PaneUuid, Vec<SerializedBlockListItem>>;
/// An AI query read from the SQLite DB.
#[derive(Identifiable, Insertable, Queryable, Selectable)]
#[diesel(table_name = ai_queries)]
#[diesel(primary_key(id))]
pub(super) struct AIQuery {
pub(super) id: i32,
pub(super) exchange_id: String,
pub(super) conversation_id: String,
pub(super) start_ts: NaiveDateTime,
pub(super) output_status: String,
pub(super) input: String,
pub(super) working_directory: Option<String>,
pub(super) model_id: String,
pub(super) coding_model_id: String,
// Planning model selection is deprecated and unused.
#[allow(unused)]
pub(super) planning_model_id: String,
}
impl TryFrom<AIQuery> for PersistedAIInput {
type Error = anyhow::Error;
fn try_from(value: AIQuery) -> Result<Self, Self::Error> {
Ok(Self {
start_ts: Local.from_utc_datetime(&value.start_ts),
inputs: serde_json::from_str(&value.input)?,
exchange_id: value.exchange_id.try_into()?,
conversation_id: value.conversation_id.try_into()?,
output_status: serde_json::from_str(&value.output_status)?,
working_directory: value.working_directory,
model_id: value.model_id.into(),
coding_model_id: value.coding_model_id.into(),
})
}
}
/// A new AI query to be inserted into the SQLite DB.
#[derive(Insertable, AsChangeset)]
#[diesel(table_name = ai_queries)]
#[diesel(treat_none_as_null = true)]
pub(super) struct NewAIQuery {
pub(super) exchange_id: String,
pub(super) conversation_id: String,
pub(super) start_ts: NaiveDateTime,
pub(super) output_status: String,
pub(super) input: String,
pub(super) working_directory: Option<String>,
pub(super) model_id: String,
}
impl TryFrom<&PersistedAIInput> for NewAIQuery {
type Error = anyhow::Error;
fn try_from(value: &PersistedAIInput) -> Result<Self, Self::Error> {
Ok(Self {
start_ts: value.start_ts.naive_utc(),
input: serde_json::to_string(&value.inputs)?,
working_directory: value.working_directory.clone(),
exchange_id: value.exchange_id.to_string(),
conversation_id: value.conversation_id.to_string(),
output_status: serde_json::to_string(&value.output_status)?,
model_id: value.model_id.clone().into(),
})
}
}
pub(super) fn read_ai_queries(
conn: &mut SqliteConnection,
) -> Result<Vec<PersistedAIInput>, diesel::result::Error> {
// Only load at most 100 AI queries; there's a very low chance that the user
// will ever try rerunning AI queries older than this duration and loading
// all AI queries in perpetuity has performance implications on app startup.
// TOOD(alokedesai): Consider loading all AI queries by paginating the SQL query.
const MAX_AI_QUERIES_TO_READ: i64 = 100;
Ok(schema::ai_queries::table
.select(AIQuery::as_select())
.order_by(schema::ai_queries::columns::start_ts.desc())
.limit(MAX_AI_QUERIES_TO_READ)
.load::<AIQuery>(conn)?
.into_iter()
.filter_map(|ai_query| PersistedAIInput::try_from(ai_query).ok())
.rev()
.collect_vec())
}
pub(super) fn upsert_ai_query(
conn: &mut SqliteConnection,
query: Arc<PersistedAIInput>,
) -> anyhow::Result<()> {
use schema::ai_queries::dsl::*;
let new_ai_query = NewAIQuery::try_from(query.as_ref())?;
Ok(conn.transaction::<_, Error, _>(|conn| {
diesel::insert_into(ai_queries)
.values(&new_ai_query)
.on_conflict(exchange_id)
.do_update()
.set(&new_ai_query)
.execute(conn)?;
Ok(())
})?)
}
/// Returns the most recent [`MAX_BLOCK_COUNT_PER_SESSION`] block list items for each session. The
/// items are in chronological order.
pub(super) fn get_all_restored_blocks(
conn: &mut SqliteConnection,
) -> Result<PersistedBlocks, diesel::result::Error> {
let terminal_sessions = schema::terminal_panes::table
.select(model::TerminalSession::as_select())
.load::<model::TerminalSession>(conn)?;
let block_lists = Block::belonging_to(&terminal_sessions)
.select(Block::as_select())
.order_by(schema::blocks::columns::id.asc())
.load::<Block>(conn)?
.grouped_by(&terminal_sessions);
let mut all_block_items_by_pane = block_lists
.into_iter()
.zip(terminal_sessions)
.map(|(blocks, terminal_pane)| {
(
PaneUuid(terminal_pane.uuid),
blocks.into_iter().map(Into::into).collect(),
)
})
.collect::<HashMap<_, Vec<SerializedBlockListItem>>>();
for (_, blocks) in all_block_items_by_pane.iter_mut() {
blocks.sort_by_key(|item| item.start_ts());
// Only keep most recent command blocks
blocks.drain(
0..blocks
.len()
.saturating_sub(MAX_TERMINAL_BLOCKS_TO_PERSIST_PER_SESSION as usize),
);
}
Ok(all_block_items_by_pane)
}
pub(super) fn save_block(
conn: &mut SqliteConnection,
pane_id: Vec<u8>,
block: &SerializedBlock,
is_local_block: bool,
) -> Result<(), Error> {
use schema::blocks::dsl::*;
conn.transaction::<_, Error, _>(|conn| {
let saved_blocks_count: i64 = schema::blocks::dsl::blocks
.filter(pane_leaf_uuid.eq(pane_id.clone()))
.filter(id.is_not_null())
.filter(is_background.ne(true))
.count()
.first(conn)?;
// add 1 because we are about to save a new block
let diff = saved_blocks_count - MAX_TERMINAL_BLOCKS_TO_PERSIST_PER_SESSION + 1;
if diff > 0 {
// Find the oldest block to keep.
let last_kept_id: Option<i32> = schema::blocks::dsl::blocks
.filter(pane_leaf_uuid.eq(pane_id.clone()))
.filter(id.is_not_null())
.filter(is_background.ne(true))
.select(id)
.order(id.asc())
.offset(diff)
.limit(1)
.first(conn)?;
if let Some(last_kept_id) = last_kept_id {
diesel::delete(
schema::blocks::dsl::blocks
.filter(id.lt(last_kept_id))
.filter(pane_leaf_uuid.eq(pane_id.clone())),
)
.execute(conn)?;
}
}
let block = create_block(pane_id, block, is_local_block);
diesel::insert_into(schema::blocks::dsl::blocks)
.values(block)
.execute(conn)?;
Ok(())
})
}
// TODO(vorporeal): can move this to a `to_persisted_block()` function on `SerializedBlock`
// to get it out of the persistence layer.
fn create_block<'a>(
pane_leaf_uuid: Vec<u8>,
block: &'a SerializedBlock,
is_local: bool,
) -> model::NewBlock<'a> {
model::NewBlock {
block_id: block.id.as_str(),
pane_leaf_uuid,
stylized_command: &block.stylized_command,
stylized_output: &block.stylized_output,
pwd: block.pwd.as_ref(),
// This sqlite column still uses the legacy `git_branch` name, but it now stores the
// block's git head for backwards compatibility with existing persisted data.
git_branch: block.git_head.as_ref(),
git_branch_name: block.git_branch_name.as_ref(),
virtual_env: block.virtual_env.as_ref(),
conda_env: block.conda_env.as_ref(),
exit_code: block.exit_code.value(),
did_execute: block.did_execute,
completed_ts: block.completed_ts.map(|ts| ts.naive_utc()),
start_ts: block.start_ts.map(|ts| ts.naive_utc()),
ps1: block.ps1.as_ref(),
rprompt: block.rprompt.as_ref(),
honor_ps1: block.honor_ps1,
is_background: block.is_background,
shell: block.shell_host.as_ref().map(|host| host.shell_type.name()),
user: block.shell_host.as_ref().map(|host| host.user.as_str()),
host: block.shell_host.as_ref().map(|host| host.hostname.as_str()),
prompt_snapshot: block.prompt_snapshot.as_ref(),
ai_metadata: block.ai_metadata.as_ref(),
is_local: Some(is_local),
agent_view_visibility: block
.agent_view_visibility
.as_ref()
.and_then(|v| serde_json::to_string(v).ok()),
}
}
pub(super) fn delete_blocks(conn: &mut SqliteConnection, pane_id: Vec<u8>) -> Result<(), Error> {
use schema::blocks::dsl::*;
conn.transaction::<_, Error, _>(|conn| {
diesel::delete(schema::blocks::dsl::blocks.filter(pane_leaf_uuid.eq(pane_id.clone())))
.execute(conn)?;
Ok(())
})
}
pub(super) fn update_block_agent_view_visibility(
conn: &mut SqliteConnection,
target_block_id: &str,
visibility: &SerializedAgentViewVisibility,
) -> anyhow::Result<()> {
use schema::blocks::dsl::*;
let visibility_json = serde_json::to_string(visibility)?;
diesel::update(blocks.filter(block_id.eq(target_block_id)))
.set(agent_view_visibility.eq(visibility_json))
.execute(conn)?;
Ok(())
}
pub(super) fn delete_ai_conversation(
conn: &mut SqliteConnection,
conversation_id_str: &str,
) -> anyhow::Result<()> {
use schema::ai_queries::dsl as queries_dsl;
conn.transaction::<_, Error, _>(|conn| {
// Delete the AI query
diesel::delete(
queries_dsl::ai_queries.filter(queries_dsl::conversation_id.eq(conversation_id_str)),
)
.execute(conn)?;
Ok(())
})?;
Ok(())
}
+72
View File
@@ -0,0 +1,72 @@
use lazy_static::lazy_static;
use session_sharing_protocol::common::{InputReplicaId, ProfileData};
use crate::{
auth::UserUid,
cloud_object::{CloudObjectGuest, ServerObjectContainer},
drive::sharing::{LinkSharingSubjectType, SharingAccessLevel, Subject, TeamKind, UserKind},
server::ids::ServerId,
};
#[test]
fn test_roundtrip_guests() {
let guests = vec![
CloudObjectGuest {
subject: Subject::User(UserKind::Account(UserUid::new("firebase_uid"))),
access_level: SharingAccessLevel::Edit,
source: None,
},
CloudObjectGuest {
subject: Subject::PendingUser {
email: Some("pending@warp.dev".to_string()),
},
access_level: SharingAccessLevel::View,
source: Some(ServerObjectContainer::Folder {
folder_uid: 123.into(),
}),
},
CloudObjectGuest {
subject: Subject::Team(TeamKind::Team {
team_uid: ServerId::from(99),
}),
access_level: SharingAccessLevel::Edit,
source: None,
},
];
let encoded = super::encode_guests(&guests).expect("encode should succeed");
let decoded = super::decode_guests(&encoded).expect("decode should succeed");
assert_eq!(guests, decoded);
}
lazy_static! {
/// By construction, [`CloudObjectGuest`] only accepts `'static`-lifetime [`Subject`]s.
///
/// In most cases, this would prevent persisting a shared session subject, but we work around
/// it here for completeness;
static ref PROFILE_DATA: ProfileData = ProfileData {
firebase_uid: "2YP93GScglXJMdEr2Id12dI7HCG3".to_string(),
display_name: "Some User".to_string(),
photo_url: Some("http://example.com/some-image".to_string()),
email: Some("user@warp.dev".to_string()),
input_replica_id: InputReplicaId::from("some-id".to_string()),
};
}
#[test]
fn test_fail_unsupported_subjects() {
let result = super::encode_guests(&[CloudObjectGuest {
subject: Subject::AnyoneWithLink(LinkSharingSubjectType::Anyone),
access_level: SharingAccessLevel::View,
source: None,
}]);
assert!(result.is_err());
let result = super::encode_guests(&[CloudObjectGuest {
subject: Subject::User(UserKind::SharedSessionParticipant(PROFILE_DATA.clone())),
access_level: SharingAccessLevel::View,
source: None,
}]);
assert!(result.is_err());
}
+10
View File
@@ -0,0 +1,10 @@
//! Supporting types for persisting cloud objects to SQLite.
pub use warp_server_client::persistence::{decode_guests, decode_link_sharing};
#[cfg(test)]
pub use warp_server_client::persistence::encode_guests;
#[cfg(test)]
#[path = "cloud_object_tests.rs"]
mod tests;
+69
View File
@@ -0,0 +1,69 @@
use anyhow::Result;
use diesel::{sqlite::SqliteConnection, ExpressionMethods, QueryDsl, RunQueryDsl};
use crate::terminal::event::UserBlockCompleted;
/// Returns the command that was run right after `command`
/// in the same session, if any.
pub fn get_next_command(
conn: &mut SqliteConnection,
command: &super::model::Command,
) -> Result<super::model::Command> {
let next_command = super::schema::commands::dsl::commands
.filter(super::schema::commands::columns::id.gt(command.id))
.filter(super::schema::commands::columns::session_id.eq(&command.session_id))
// Skip any empty blocks
.filter(super::schema::commands::columns::command.ne(""))
.order(super::schema::commands::columns::id.asc())
.limit(1)
.first::<super::model::Command>(conn)?;
Ok(next_command)
}
/// Returns the commands that were run right before `command`
/// in the same session, if any. They are ordered from oldest to newest.
pub fn get_previous_commands(
conn: &mut SqliteConnection,
command: &super::model::Command,
num_commands: usize,
) -> Result<Vec<super::model::Command>> {
let previous_commands = super::schema::commands::dsl::commands
.filter(super::schema::commands::columns::id.lt(command.id))
.filter(super::schema::commands::columns::session_id.eq(&command.session_id))
// Skip any empty blocks
.filter(super::schema::commands::columns::command.ne(""))
.order(super::schema::commands::columns::id.desc())
.limit(num_commands as i64)
.load::<super::model::Command>(conn)?;
Ok(previous_commands.into_iter().rev().collect())
}
/// Gets the last num_commands times the same command was run in a similar context
/// (same pwd, exit code, shell, hostname), from newest to oldest.
pub fn get_same_commands_from_history(
conn: &mut SqliteConnection,
completed_block: &UserBlockCompleted,
num_commands: usize,
) -> Result<Vec<super::model::Command>> {
let shell_host = completed_block.serialized_block.shell_host.as_ref();
let commands = super::schema::commands::dsl::commands
.filter(super::schema::commands::columns::command.eq(&completed_block.command))
.filter(super::schema::commands::columns::pwd.eq(&completed_block.serialized_block.pwd))
.filter(
super::schema::commands::columns::exit_code
.eq(completed_block.serialized_block.exit_code.value()),
)
.filter(
super::schema::commands::columns::shell
.eq(shell_host.map(|host| host.shell_type.name())),
)
.filter(
super::schema::commands::columns::hostname.eq(shell_host.map(|host| &host.hostname)),
)
// Get newest to oldest commands.
.order(super::schema::commands::columns::id.desc())
.limit(num_commands as i64)
.load::<super::model::Command>(conn)?;
Ok(commands)
}
+388
View File
@@ -0,0 +1,388 @@
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
pub mod agent;
mod block_list;
mod cloud_objects;
mod sqlite;
pub mod commands;
}
}
pub use persistence::model;
#[cfg_attr(not(feature = "local_fs"), expect(unused_imports))]
pub use persistence::schema;
#[cfg(feature = "integration_tests")]
pub mod testing;
use instant::Instant;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use std::thread::JoinHandle;
use crate::ai::persisted_workspace::EnablementState;
use ai::project_context::model::ProjectRulePath;
use chrono::{DateTime, Local, Utc};
use lsp::supported_servers::LSPServerType;
use uuid::Uuid;
use warp_core::command::ExitCode;
use warp_graphql::scalars::time::ServerTimestamp;
use warp_multi_agent_api as api;
use warpui::{AppContext, Entity, SingletonEntity};
use crate::ai::blocklist::PersistedAIInput;
use crate::ai::mcp::TemplatableMCPServerInstallation;
use crate::app_state::AppState;
use crate::auth::auth_manager::PersistedCurrentUserInformation;
use crate::cloud_object::model::actions::ObjectAction;
use crate::cloud_object::model::generic_string_model::CloudStringObject;
use crate::cloud_object::{
CloudObject, CloudObjectMetadata, ObjectIdType, RevisionAndLastEditor, ServerCreationInfo,
};
use crate::drive::folders::CloudFolder;
use crate::notebooks::CloudNotebook;
use crate::server::experiments::ServerExperiment;
use crate::server::ids::SyncId;
use crate::suggestions::ignored_suggestions_model::SuggestionType;
use crate::terminal::history::PersistedCommand;
use crate::terminal::model::block::{SerializedAgentViewVisibility, SerializedBlock};
use crate::terminal::model::session::SessionId;
use crate::workflows::CloudWorkflow;
use crate::workspaces::user_profiles::UserProfileWithUID;
use crate::workspaces::workspace::{Workspace as WorkspaceMetadata, WorkspaceUid};
use ai::workspace::WorkspaceMetadata as CodeWorkspaceMetadata;
use self::model::{AgentConversation, AgentConversationData, Project};
#[cfg(any(feature = "local_fs", feature = "integration_tests"))]
pub use sqlite::database_file_path;
#[cfg(any(feature = "local_fs", feature = "integration_tests"))]
pub use sqlite::establish_ro_connection;
/// Initializes the persistence "subsystem".
///
/// Returns the previously-persisted data, if any, and handles for
/// writing updated data to persist, if the persistence subsystem is
/// available.
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
pub fn initialize(ctx: &mut AppContext) -> (Option<PersistedData>, Option<WriterHandles>) {
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
sqlite::initialize(ctx)
} else {
(None, None)
}
}
}
// Remove sqlite database as part of Logout v0.
// TODO: Implement per user scoping of sqlite.
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
pub fn remove(sender: &Option<SyncSender<ModelEvent>>) {
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
if let Some(sender) = sender.clone() {
sqlite::remove(sender);
}
} else {
log::info!("Local filesystem persistence is not enabled.");
}
}
}
// Reconstruct sqlite database as part of Logout v0.
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
pub fn reconstruct(sender: &Option<SyncSender<ModelEvent>>) {
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
if let Some(sender) = sender.clone() {
sqlite::reconstruct(sender);
}
} else {
log::info!("Local filesystem persistence is not enabled.");
}
}
}
/// Holds interfaces to the writer thread.
pub struct WriterHandles {
pub handle: JoinHandle<()>,
pub sender: SyncSender<ModelEvent>,
}
/// Model for interacting with the writer thread.
pub struct PersistenceWriter {
thread_handle: Option<JoinHandle<()>>,
model_event_sender: Option<SyncSender<ModelEvent>>,
}
impl PersistenceWriter {
pub fn new(handle: Option<WriterHandles>) -> Self {
let (thread_handle, model_event_sender) = match handle {
Some(handle) => (Some(handle.handle), Some(handle.sender)),
None => (None, None),
};
Self {
thread_handle,
model_event_sender,
}
}
/// Sending half for sending model updates to the persistence writer thread.
pub fn sender(&self) -> Option<SyncSender<ModelEvent>> {
self.model_event_sender.clone()
}
/// Synchronously terminate the SQLite writer thread.
pub fn terminate(&mut self) {
if let Some(handle) = self.thread_handle.take() {
let start = Instant::now();
let Some(sender) = self.sender() else {
log::error!("Model event sender should exist if thread handle is set");
return;
};
if let Err(err) = sender.send(ModelEvent::Terminate) {
log::error!("Could not terminate SQLite writer thread: {err}");
}
if handle.join().is_err() {
// If crash reporting is enabled, Sentry will have already handled the panic.
log::error!("SQLite writer thread panicked");
}
log::info!("Shut down SQLite writer in {:?}", start.elapsed());
}
}
}
impl Drop for PersistenceWriter {
fn drop(&mut self) {
self.terminate();
}
}
impl Entity for PersistenceWriter {
type Event = ();
}
impl SingletonEntity for PersistenceWriter {}
/// TODO: all of this data should eventually be indexed by user_id so that
/// the logged in user sees the data for their user (and if another user logs in,
/// they see their respective data). To do this, we can simply return a mapping
/// of user ID->SqliteData and get the respective AppState after the user logs in.
///
/// For now, to address the global scoping here, we clear all persisted data on logout.
pub struct PersistedData {
/// Session restoration data
pub app_state: AppState,
/// Shareable objects.
pub cloud_objects: Vec<Box<dyn CloudObject>>,
pub workspaces: Vec<WorkspaceMetadata>,
pub current_workspace_uid: Option<WorkspaceUid>,
pub command_history: Vec<PersistedCommand>,
pub user_profiles: Vec<UserProfileWithUID>,
pub time_of_next_force_object_refresh: Option<DateTime<Utc>>,
pub object_actions: Vec<ObjectAction>,
pub experiments: Vec<ServerExperiment>,
pub ai_queries: Vec<PersistedAIInput>,
pub codebase_indices: Vec<CodeWorkspaceMetadata>,
pub workspace_language_servers: HashMap<PathBuf, HashMap<LSPServerType, EnablementState>>,
pub multi_agent_conversations: Vec<AgentConversation>,
pub projects: Vec<Project>,
pub project_rules: Vec<ProjectRulePath>,
pub ignored_suggestions: Vec<(String, SuggestionType)>,
pub mcp_server_installations: HashMap<Uuid, TemplatableMCPServerInstallation>,
pub mcp_servers_to_restore: Vec<Uuid>,
}
#[derive(Clone, Debug)]
pub struct BlockCompleted {
pub pane_id: Vec<u8>,
/// Indicates if the block was created locally (e.g. not in a remote session)
pub is_local: bool,
pub block: Arc<SerializedBlock>,
}
#[derive(Debug)]
pub struct StartedCommandMetadata {
pub command: String,
pub start_ts: Option<DateTime<Local>>,
pub pwd: Option<String>,
pub shell: Option<String>,
pub username: Option<String>,
pub hostname: Option<String>,
pub session_id: Option<SessionId>,
pub git_branch: Option<String>,
pub cloud_workflow_id: Option<SyncId>,
pub workflow_command: Option<String>,
pub is_agent_executed: bool,
}
#[derive(Debug)]
pub struct FinishedCommandMetadata {
pub exit_code: ExitCode,
pub start_ts: DateTime<Local>,
pub completed_ts: DateTime<Local>,
pub session_id: SessionId,
}
#[derive(Debug)]
pub enum ModelEvent {
SaveBlock(BlockCompleted),
DeleteBlocks(Vec<u8>),
Snapshot(AppState),
UpsertWorkflows(Vec<CloudWorkflow>),
UpsertNotebooks(Vec<CloudNotebook>),
UpsertFolders(Vec<CloudFolder>),
MarkObjectAsSynced {
hashed_sqlite_id: String,
revision_and_editor: RevisionAndLastEditor,
metadata_ts: Option<ServerTimestamp>,
},
IncrementRetryCount(String),
UpsertGenericStringObject {
object: Box<dyn CloudStringObject>,
},
UpsertGenericStringObjects(Vec<Box<dyn CloudStringObject>>),
UpsertNotebook {
notebook: CloudNotebook,
},
UpsertWorkflow {
workflow: CloudWorkflow,
},
UpsertFolder {
folder: CloudFolder,
},
UpdateObjectAfterServerCreation {
client_id: String,
server_creation_info: ServerCreationInfo,
},
DeleteObjects {
ids: Vec<(SyncId, ObjectIdType)>,
},
UpsertWorkspace {
workspace: Box<WorkspaceMetadata>,
},
UpsertWorkspaces {
workspaces: Vec<WorkspaceMetadata>,
},
SetCurrentWorkspace {
workspace_uid: WorkspaceUid,
},
UpdateObjectMetadata {
id: String,
metadata: CloudObjectMetadata,
},
InsertCommand {
metadata: StartedCommandMetadata,
},
UpdateFinishedCommand {
metadata: FinishedCommandMetadata,
},
UpsertUserProfiles {
profiles: Vec<UserProfileWithUID>,
},
ClearUserProfiles,
RecordTimeOfNextRefresh {
timestamp: DateTime<Utc>,
},
SaveExperiments {
experiments: Vec<ServerExperiment>,
},
// `PauseAndRemoveDatabase` and `ReconstructAndResume` are used to pause and resume the writer thread.
// These are employed as part of Logout v0 to ensure that the writer thread
// does not continue writing to the DB after the user has logged out and the DB is deleted.
PauseAndRemoveDatabase,
#[cfg(feature = "local_fs")]
ReconstructAndResume,
InsertObjectAction {
object_action: ObjectAction,
},
SyncObjectActions {
actions_to_sync: Vec<ObjectAction>,
},
/// Close the SQLite writer thread when the app is about to quit.
Terminate,
UpsertAIQuery {
query: Arc<PersistedAIInput>,
},
/// Delete the AI query and related data for a given conversation.
DeleteAIConversation {
conversation_id: String,
},
UpdateMultiAgentConversation {
conversation_id: String,
updated_tasks: Vec<api::Task>,
conversation_data: AgentConversationData,
},
DeleteMultiAgentConversations {
conversation_ids: Vec<String>,
},
UpsertCurrentUserInformation {
user_information: PersistedCurrentUserInformation,
},
UpsertCodebaseIndexMetadata {
index_metadata: Box<CodeWorkspaceMetadata>,
},
DeleteCodebaseIndexMetadata {
repo_path: PathBuf,
},
UpsertProject {
project: Project,
},
DeleteProject {
path: String,
},
UpsertMCPServerEnvironmentVariables {
mcp_server_uuid: Vec<u8>,
environment_variables: String,
},
UpsertProjectRules {
project_rule_paths: Vec<ProjectRulePath>,
},
DeleteProjectRules {
path: Vec<PathBuf>,
},
AddIgnoredSuggestion {
suggestion: String,
suggestion_type: SuggestionType,
},
RemoveIgnoredSuggestion {
suggestion: String,
suggestion_type: SuggestionType,
},
UpsertMCPServerInstallation {
mcp_server_installation: TemplatableMCPServerInstallation,
},
DeleteMCPServerInstallations {
installation_uuids: Vec<Uuid>,
},
DeleteMCPServerInstallationsByTemplateUuid {
template_uuid: Uuid,
},
UpdateMCPInstallationRunning {
installation_uuid: Uuid,
running: bool,
},
UpsertWorkspaceLanguageServer {
workspace_path: PathBuf,
lsp_type: LSPServerType,
enabled: EnablementState,
},
UpdateBlockAgentViewVisibility {
block_id: String,
agent_view_visibility: SerializedAgentViewVisibility,
},
SaveAIDocumentContent {
document_id: String,
content: String,
version: i32,
title: String,
},
}
File diff suppressed because it is too large Load Diff
+417
View File
@@ -0,0 +1,417 @@
use std::{path::PathBuf, sync::Arc};
use warp_core::features::FeatureFlag;
use warp_graphql::scalars::time::ServerTimestamp;
use crate::{
app_state::{
AppState, CodePaneSnapShot, CodePaneTabSnapshot, LeafContents, LeafSnapshot,
PaneNodeSnapshot, TabSnapshot, TerminalPaneSnapshot, WindowSnapshot,
},
cloud_object::{CloudObjectPermissions, Owner},
code::editor_management::CodeSource,
notebooks::{CloudNotebook, CloudNotebookModel},
persistence::{model::ObjectPermissions, BlockCompleted, ModelEvent},
server::ids::ClientId,
tab::SelectedTabColor,
terminal::model::block::SerializedBlock,
terminal::ShellLaunchData,
};
use super::{
decode_path, deduplicate_events, encode_path, read_sqlite_data, save_app_state, setup_database,
};
#[test]
fn test_deduplicate_snapshots() {
let local_notebook = CloudNotebook::new_local(
CloudNotebookModel {
title: "Hello".to_string(),
data: "World".to_string(),
ai_document_id: None,
conversation_id: None,
},
Owner::mock_current_user(),
None,
ClientId::new(),
);
let completed_block_1 = BlockCompleted {
pane_id: vec![1, 2, 3],
block: Arc::new(SerializedBlock::default()),
is_local: true,
};
let completed_block_2 = BlockCompleted {
pane_id: vec![4, 5, 6],
block: Arc::new(SerializedBlock::default()),
is_local: true,
};
let snapshot_1 = AppState {
active_window_index: Some(1),
block_lists: Default::default(),
windows: Default::default(),
running_mcp_servers: Default::default(),
};
let snapshot_2 = AppState {
active_window_index: Some(2),
block_lists: Default::default(),
windows: Default::default(),
running_mcp_servers: Default::default(),
};
let snapshot_3 = AppState {
active_window_index: Some(3),
block_lists: Default::default(),
windows: Default::default(),
running_mcp_servers: Default::default(),
};
let original_events = vec![
ModelEvent::UpsertNotebook {
notebook: local_notebook.clone(),
},
ModelEvent::Snapshot(snapshot_1.clone()),
ModelEvent::SaveBlock(completed_block_1.clone()),
ModelEvent::Snapshot(snapshot_2.clone()),
ModelEvent::SaveBlock(completed_block_2.clone()),
ModelEvent::Snapshot(snapshot_3.clone()),
ModelEvent::UpsertNotebook {
notebook: local_notebook.clone(),
},
];
let filtered_events = deduplicate_events(original_events);
assert_eq!(filtered_events.len(), 5);
assert!(matches!(
&filtered_events[0],
&ModelEvent::UpsertNotebook { .. }
));
// The first snapshot should have been filtered out.
assert!(matches!(&filtered_events[1], &ModelEvent::SaveBlock(_)));
// The second snapshot should have been filtered out.
assert!(matches!(&filtered_events[2], &ModelEvent::SaveBlock(_)));
// The third snapshot should be preserved.
match &filtered_events[3] {
ModelEvent::Snapshot(snapshot) => assert_eq!(snapshot, &snapshot_3),
other => panic!("Expected ModelEvent::Snapshot, got {other:?}"),
}
assert!(matches!(
&filtered_events[4],
&ModelEvent::UpsertNotebook { .. }
));
}
#[test]
fn test_deduplicate_no_snapshots() {
let original_events = vec![ModelEvent::SaveBlock(BlockCompleted {
pane_id: vec![1, 2, 3],
block: Default::default(),
is_local: true,
})];
let filtered_events = deduplicate_events(original_events);
assert_eq!(filtered_events.len(), 1);
assert!(matches!(&filtered_events[0], &ModelEvent::SaveBlock(_)));
}
fn test_terminal_window_snapshot(vertical_tabs_panel_open: bool) -> WindowSnapshot {
WindowSnapshot {
tabs: vec![TabSnapshot {
custom_title: None,
root: PaneNodeSnapshot::Leaf(LeafSnapshot {
is_focused: true,
custom_vertical_tabs_title: None,
contents: LeafContents::Terminal(TerminalPaneSnapshot {
uuid: vec![u8::from(vertical_tabs_panel_open) + 1],
cwd: Some("/tmp".to_string()),
shell_launch_data: Some(ShellLaunchData::Executable {
executable_path: PathBuf::from("/bin/zsh"),
shell_type: crate::terminal::shell::ShellType::Zsh,
}),
is_active: true,
is_read_only: false,
input_config: None,
llm_model_override: None,
active_profile_id: None,
conversation_ids_to_restore: vec![],
active_conversation_id: None,
}),
}),
default_directory_color: None,
selected_color: SelectedTabColor::default(),
left_panel: None,
right_panel: None,
}],
active_tab_index: 0,
bounds: None,
fullscreen_state: Default::default(),
quake_mode: false,
universal_search_width: None,
warp_ai_width: None,
voltron_width: None,
warp_drive_index_width: None,
left_panel_open: false,
vertical_tabs_panel_open,
left_panel_width: None,
right_panel_width: None,
agent_management_filters: None,
}
}
#[test]
fn test_sqlite_round_trips_vertical_tabs_panel_open() {
let tempdir = tempfile::tempdir().expect("tempdir should be created");
let database_path = tempdir.path().join("warp.sqlite");
let mut conn = setup_database(&database_path).expect("database should initialize");
let app_state = AppState {
windows: vec![
test_terminal_window_snapshot(false),
test_terminal_window_snapshot(true),
],
active_window_index: Some(1),
block_lists: Default::default(),
running_mcp_servers: Default::default(),
};
save_app_state(&mut conn, &app_state).expect("app state should save");
let restored = read_sqlite_data(&mut conn, None)
.expect("app state should load")
.app_state;
assert_eq!(restored.active_window_index, Some(1));
assert_eq!(
restored
.windows
.iter()
.map(|window| window.vertical_tabs_panel_open)
.collect::<Vec<_>>(),
vec![false, true]
);
}
#[test]
fn test_sqlite_round_trips_custom_vertical_tabs_title() {
let tempdir = tempfile::tempdir().expect("tempdir should be created");
let database_path = tempdir.path().join("warp.sqlite");
let mut conn = setup_database(&database_path).expect("database should initialize");
let app_state = AppState {
windows: vec![WindowSnapshot {
tabs: vec![TabSnapshot {
custom_title: None,
root: PaneNodeSnapshot::Leaf(LeafSnapshot {
is_focused: true,
custom_vertical_tabs_title: Some("Production API".to_string()),
contents: LeafContents::Terminal(TerminalPaneSnapshot {
uuid: vec![42],
cwd: Some("/tmp".to_string()),
shell_launch_data: Some(ShellLaunchData::Executable {
executable_path: PathBuf::from("/bin/zsh"),
shell_type: crate::terminal::shell::ShellType::Zsh,
}),
is_active: true,
is_read_only: false,
input_config: None,
llm_model_override: None,
active_profile_id: None,
conversation_ids_to_restore: vec![],
active_conversation_id: None,
}),
}),
default_directory_color: None,
selected_color: SelectedTabColor::default(),
left_panel: None,
right_panel: None,
}],
active_tab_index: 0,
bounds: None,
fullscreen_state: Default::default(),
quake_mode: false,
universal_search_width: None,
warp_ai_width: None,
voltron_width: None,
warp_drive_index_width: None,
left_panel_open: false,
vertical_tabs_panel_open: false,
left_panel_width: None,
right_panel_width: None,
agent_management_filters: None,
}],
active_window_index: Some(0),
block_lists: Default::default(),
running_mcp_servers: Default::default(),
};
save_app_state(&mut conn, &app_state).expect("app state should save");
let restored = read_sqlite_data(&mut conn, None)
.expect("app state should load")
.app_state;
let PaneNodeSnapshot::Leaf(LeafSnapshot {
custom_vertical_tabs_title,
..
}) = &restored.windows[0].tabs[0].root
else {
panic!("Expected terminal pane leaf");
};
assert_eq!(
custom_vertical_tabs_title.as_deref(),
Some("Production API")
);
}
#[test]
fn test_sqlite_round_trips_code_pane_with_multiple_tabs() {
let tempdir = tempfile::tempdir().expect("tempdir should be created");
let database_path = tempdir.path().join("warp.sqlite");
let mut conn = setup_database(&database_path).expect("database should initialize");
let app_state = AppState {
windows: vec![WindowSnapshot {
tabs: vec![TabSnapshot {
custom_title: None,
root: PaneNodeSnapshot::Leaf(LeafSnapshot {
is_focused: true,
custom_vertical_tabs_title: None,
contents: LeafContents::Code(CodePaneSnapShot::Local {
tabs: vec![
CodePaneTabSnapshot {
path: Some(PathBuf::from("/tmp/main.rs")),
},
CodePaneTabSnapshot {
path: Some(PathBuf::from("/tmp/lib.rs")),
},
CodePaneTabSnapshot { path: None },
],
active_tab_index: 1,
source: Some(CodeSource::FileTree {
path: PathBuf::from("/tmp/main.rs"),
}),
}),
}),
default_directory_color: None,
selected_color: SelectedTabColor::default(),
left_panel: None,
right_panel: None,
}],
active_tab_index: 0,
bounds: None,
fullscreen_state: Default::default(),
quake_mode: false,
universal_search_width: None,
warp_ai_width: None,
voltron_width: None,
warp_drive_index_width: None,
left_panel_open: false,
vertical_tabs_panel_open: false,
left_panel_width: None,
right_panel_width: None,
agent_management_filters: None,
}],
active_window_index: Some(0),
block_lists: Default::default(),
running_mcp_servers: Default::default(),
};
save_app_state(&mut conn, &app_state).expect("app state should save");
let restored = read_sqlite_data(&mut conn, None)
.expect("app state should load")
.app_state;
assert_eq!(restored.windows.len(), 1);
let restored_tab = &restored.windows[0].tabs[0];
let PaneNodeSnapshot::Leaf(LeafSnapshot {
contents:
LeafContents::Code(CodePaneSnapShot::Local {
tabs,
active_tab_index,
source,
}),
..
}) = &restored_tab.root
else {
panic!("Expected code pane leaf");
};
assert_eq!(tabs.len(), 3);
assert_eq!(*active_tab_index, 1);
assert_eq!(tabs[0].path, Some(PathBuf::from("/tmp/main.rs")));
assert_eq!(tabs[1].path, Some(PathBuf::from("/tmp/lib.rs")));
assert_eq!(tabs[2].path, None);
assert!(matches!(source, Some(CodeSource::FileTree { .. })));
}
fn assert_encode_then_decode_preserves_original_path(original_path: PathBuf) {
let bytes = encode_path(original_path.clone());
let decoded_path = decode_path(bytes);
assert_eq!(original_path, decoded_path);
}
/// Test that a local path can be encoded and decoded. We use this when persisting a local
/// file path for notebooks in sqlite. We need this test because Windows `OsString`s are
/// often arbitrary sequences of 16-bit values, unlike Unix which uses sequences of 8-bit
/// values (bytes). Since `diesel::sql_types::Binary` deals with sequences of bytes (`u8`)
/// we need to perform special casting on `OsString`s on Windows.
#[test]
fn test_path_encode_decode() {
// Empty path
assert_encode_then_decode_preserves_original_path(PathBuf::new());
// Windows-style paths
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"C:\windows\system32.dll"));
assert_encode_then_decode_preserves_original_path(PathBuf::from("c:temp"));
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"\temp"));
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"\temp\emoji\🙈.txt"));
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"\temp\ñoñàscii\temp.txt"));
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"\temp\hindi\हिन्दी"));
assert_encode_then_decode_preserves_original_path(PathBuf::from(r"\temp\cjk\狗没有耐心"));
// Unix-style paths
assert_encode_then_decode_preserves_original_path(PathBuf::from(
"/home/persistence/example.sql",
));
assert_encode_then_decode_preserves_original_path(PathBuf::from("./database/log.txt"));
assert_encode_then_decode_preserves_original_path(PathBuf::from("/temp/emoji/🙈.txt"));
assert_encode_then_decode_preserves_original_path(PathBuf::from("/temp/ñoñàscii/temp.txt"));
assert_encode_then_decode_preserves_original_path(PathBuf::from("/temp/hindi/हिन्दी"));
assert_encode_then_decode_preserves_original_path(PathBuf::from("/temp/cjk/狗没有耐心"));
}
#[test]
fn test_deserialize_corrupted_guests() {
let _ = FeatureFlag::SharedWithMe.override_enabled(true);
// Use a hardcoded timestamp to ensure this test works on systems with more-than-microsecond
// precision.
let permissions_ts_micros = 123456;
let permissions_ts =
ServerTimestamp::from_unix_timestamp_micros(permissions_ts_micros).unwrap();
let db_permissions = ObjectPermissions {
id: 42,
object_metadata_id: 10,
subject_type: "TEAM".to_string(),
subject_id: Some("7".to_string()),
subject_uid: "team_uid12345678912345".to_string(),
permissions_last_updated_at: Some(permissions_ts_micros),
// This is not a valid set of encoded object guests.
object_guests: Some(vec![1, 2, 3]),
anyone_with_link_access_level: None,
anyone_with_link_source: None,
};
// The overall permissions should successfully convert, minus the object guests.
let cloud_permissions = super::to_cloud_object_permissions(&db_permissions, None);
assert_eq!(
cloud_permissions,
Some(CloudObjectPermissions {
owner: Owner::Team {
team_uid: crate::server::ids::ServerId::from_string_lossy("team_uid12345678912345"),
},
permissions_last_updated_ts: Some(permissions_ts),
anyone_with_link: None,
guests: vec![],
})
);
}
+46
View File
@@ -0,0 +1,46 @@
//! Module with integration test-only util methods setting up sqlite.
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use super::{schema, sqlite::init_db};
/// Updates the 'user' and 'host' columns for stored blocks to the given values.
///
/// This is used at runtime to update the user and host values to real values based on the running
/// machine in integration tests that rely on accuracy of these values.
pub fn set_user_and_hostname_for_blocks(user: String, hostname: String) {
let mut conn = init_db().expect("Should be able to establish sqlite connection.");
// Update the 'user' and 'host' columns to their real values (based on the machine on which this test is running)
// for blocks that were stored with the placeholder 'local:user' and 'local:host' values.
//
// This allows us to use real (rather than mocked out) logic for matching restored
// blocks to the appropriate session based on session hostnamebased on system hostname.
diesel::update(schema::blocks::dsl::blocks.filter(schema::blocks::user.eq("local:user")))
.set((
schema::blocks::user.eq(user),
schema::blocks::host.eq(hostname),
))
.execute(&mut conn)
.expect("Failed to update user and hostname for restored blocks.");
}
pub fn set_user_and_hostname_for_commands(user: String, hostname: String) {
let mut conn = init_db().expect("Should be able to establish sqlite connection.");
// Update the 'user' and 'host' columns to their real values (based on the machine on which
// this test is running) for commands that were stored with the placeholder 'local:user' and
// 'local:host' values.
//
// This allows us to use real (rather than mocked out) logic for matching history commands to
// the appropriate session based on session hostnamebased on system hostname.
diesel::update(
schema::commands::dsl::commands.filter(schema::commands::username.eq("local:user")),
)
.set((
schema::commands::username.eq(user),
schema::commands::hostname.eq(hostname),
))
.execute(&mut conn)
.expect("Failed to update user and hostname for persisted commands.");
}