use nom_language::error::{VerboseError, VerboseErrorKind};
use serde::{Deserialize, Serialize};
use std::{fmt::Display, str::FromStr};
use thiserror::Error;
use crate::ast::Json;
wire_enum! {
pub enum ErrorCategory {
Syntax = "syntax",
Protocol = "protocol",
Schema = "schema",
Data = "data",
Epistemic = "epistemic",
Governance = "governance",
Transaction = "transaction",
History = "history",
Search = "search",
Artifact = "artifact",
Resource = "resource",
Transport = "transport",
System = "system",
}
}
wire_enum! {
pub enum RetryClass {
SafeSameRequest = "safe_same_request",
RequiresRefresh = "requires_refresh",
RequiresDifferentInput = "requires_different_input",
RequiresAuthority = "requires_authority",
RequiresNewSnapshot = "requires_new_snapshot",
RequiresReacquireArtifact = "requires_reacquire_artifact",
OutcomeLookupRequired = "outcome_lookup_required",
NonRetryable = "non_retryable",
}
}
macro_rules! kip_error_codes {
(@retry) => { RetryClass::RequiresDifferentInput };
(@retry $retry:ident) => { RetryClass::$retry };
($(
$(#[$meta:meta])*
$variant:ident : $category:ident $(, $retry:ident)? => $hint:expr
);+ $(;)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum KipErrorCode {
$( $(#[$meta])* $variant, )+
}
impl KipErrorCode {
pub const ALL: &'static [KipErrorCode] = &[ $( KipErrorCode::$variant, )+ ];
pub fn name(&self) -> &'static str {
match self {
$( KipErrorCode::$variant => stringify!($variant), )+
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
$( stringify!($variant) => Some(KipErrorCode::$variant), )+
_ => None,
}
}
pub fn category(&self) -> ErrorCategory {
match self {
$( KipErrorCode::$variant => ErrorCategory::$category, )+
}
}
pub fn retry_class(&self) -> RetryClass {
match self {
$( KipErrorCode::$variant => kip_error_codes!(@retry $($retry)?), )+
}
}
pub fn hint(&self) -> &'static str {
match self {
$( KipErrorCode::$variant => $hint, )+
}
}
}
};
}
kip_error_codes! {
InvalidSyntax: Syntax =>
"Check bracket matching, keyword spelling and clause order. Run `VALIDATE \
KQL`/`VALIDATE KML` on the text before re-sending.";
InvalidIdentifier: Syntax =>
"Identifiers must match `[A-Za-z_][A-Za-z0-9_]*`.";
InvalidRequestEnvelope: Protocol =>
"Check the envelope: `kip` version, `operations[]` shape, and that `execution.mode` is \
one of independent, sequence, atomic.";
UnsupportedProtocolVersion: Protocol, NonRetryable =>
"Run `DESCRIBE PROTOCOL` to learn which protocol versions this runtime speaks.";
UnsupportedCapability: Protocol, NonRetryable =>
"Run `DESCRIBE CAPABILITIES` and request only what is both supported and available.";
UnsupportedIsolation: Protocol, NonRetryable =>
"Run `DESCRIBE CAPABILITIES` for the isolation levels this runtime offers.";
LanguageMismatch: Protocol =>
"The `language` label must match the command's real semantics; a KML write cannot be \
labelled KQL.";
ReadonlyViolation: Protocol =>
"This endpoint executes KQL and META only. Re-send state-changing KML through the \
general runtime.";
DuplicateLocalHandle: Protocol =>
"Two clauses claim the same `?handle`. Rename one: forward references must resolve to \
exactly one clause.";
DuplicateMutationTarget: Protocol =>
"One transaction may mutate an element once. Merge the two clauses into a single \
mutation.";
SchemaSymbolNotFound: Schema =>
"Run `LIST TYPES` / `LIST PREDICATES` or `DESCRIBE TYPE` to confirm the symbol. Symbols \
are case-sensitive.";
SchemaSymbolAmbiguous: Schema =>
"The local name resolves in more than one package. Qualify it with its package path.";
SchemaFieldNotFound: Schema =>
"Run `DESCRIBE TYPE` / `DESCRIBE FACET` to see which fields the element actually \
declares.";
SchemaPackageUnavailable: Schema, RequiresRefresh =>
"Run `LIST SCHEMA PACKAGES` to check what is active in this Schema Environment.";
SchemaEnvironmentChanged: Schema, RequiresRefresh =>
"The environment changed under the request. Re-read `DESCRIBE SCHEMA ENVIRONMENT` and \
retry.";
HistoricalSchemaUnavailable: Schema, NonRetryable =>
"The Schema needed to interpret that history is no longer retained; the historical read \
cannot be served.";
TypeMismatch: Schema =>
"Correct the value's type to match its declaration.";
ConstraintViolation: Schema =>
"Supply the missing required fields, or relax the value to satisfy the constraint.";
NotFoundOrNotVisible: Data =>
"The target does not exist or is not visible to you. Ground with `SEARCH` and confirm \
with an exact id before writing.";
ReferenceError: Data =>
"Bind the variable in the WHERE block, or create the handle earlier in the same MUTATE \
plan.";
StructuralReferenceInvalid: Data =>
"Run `DESCRIBE STRUCTURAL FIELD` for the field's legal target kinds and cardinality.";
IdentitySelectorRequired: Data =>
"Add a stable selector: `{id: ...}` or `{key: ...}`.";
NameIdentityForbidden: Data =>
"`name` is mutable grounding state and never identifies an element. Match on `id` or \
`key`.";
IdentityConflict: Data =>
"Two identity claims disagree. Resolve which element you mean before retrying.";
ClientKeyConflict: Data =>
"That `client_key` already names a different element. Use a fresh key, or address the \
existing element by id.";
IdentityMergeConflict: Data =>
"The two Concepts cannot be merged. Inspect both with `DESCRIBE`/`FIND` before deciding \
a canonical target.";
ImmutableField: Epistemic =>
"The field is immutable after creation; express the change as new state instead.";
EpistemicRevisionRequired: Epistemic =>
"An Assertion's epistemic payload never changes. Record a new Assertion with `ASSERT \
... SUPERSEDING :old`, or `TRANSITION :old TO \"superseded\" BY :new`.";
EvidenceCorrectionRequired: Epistemic =>
"Evidence payload never changes. Record the corrected Evidence and `TRANSITION :old TO \
\"corrected\" BY :new`.";
InvalidLifecycleTransition: Epistemic =>
"Read the element's current lifecycle state first (`details.from` / `details.to`); that \
TRANSITION is not legal from where it is, or not for its kind.";
RetractionNotAuthorized: Epistemic, RequiresAuthority =>
"Only the assertor may retract their own Assertion.";
SupersessionMismatch: Epistemic, RequiresRefresh =>
"The superseding Assertion must address the same slot as the one it supersedes.";
EvidenceCorrectionConflict: Epistemic, RequiresRefresh =>
"That Evidence already has a conflicting correction. Re-read its lineage.";
ActivityTerminal: Epistemic =>
"A terminal Activity is immutable. Finalize outputs in the same `TRANSITION ... TO \
\"completed\" SET STRUCTURAL` that ends it.";
ProjectionTargetUnbound: Epistemic =>
"Bind the projection's Proposition in the WHERE block first.";
ProjectionTargetUnbounded: Epistemic =>
"BELIEF needs a bounded target: name the Proposition, or ground the subject and \
predicate.";
ProjectionNotAuthorized: Epistemic, RequiresAuthority =>
"You may read the raw claims but not project belief here.";
ProjectionPolicyUnavailable: Epistemic, RequiresRefresh =>
"Run `LIST EPISTEMIC POLICIES` / `DESCRIBE EPISTEMIC POLICY` to see what can be \
projected with.";
Unauthenticated: Governance, RequiresAuthority =>
"Authenticate before issuing this request.";
NotAuthorized: Governance, RequiresAuthority =>
"Run `DESCRIBE ACCESS` to see which operations you may perform here.";
RequiresApproval: Governance, RequiresAuthority =>
"The operation is queued behind an out-of-band approval.";
RequiresStrongerAuthentication: Governance, RequiresAuthority =>
"Re-authenticate with a stronger factor and retry.";
ActorBindingRequired: Governance, RequiresAuthority =>
"Attribution needs an ActorBinding: you cannot assert on behalf of an actor you are not \
bound to.";
ProtectedSystemField: Governance, NonRetryable =>
"`_system` is engine truth and is never written by a mutation.";
ProtectedGovernanceField: Governance, NonRetryable =>
"Governance lives in the protected control plane, not in cognitive mutations.";
ProtectedSchemaState: Governance, NonRetryable =>
"Schema state is immutable Package state; publish and activate a Package instead.";
LegalHoldConflict: Governance, RequiresAuthority =>
"A legal hold covers this element; removal is blocked until it is lifted.";
PurgeDenied: Governance, RequiresAuthority =>
"Physical purge was denied by policy.";
VersionConflict: Transaction, RequiresRefresh =>
"The element changed since you read it. Re-read it, re-apply your change, and retry \
with the fresh `EXPECT VERSION`.";
PreconditionFailed: Transaction, RequiresRefresh =>
"A declared precondition no longer holds. Re-read the current state and retry.";
SerializationConflict: Transaction, SafeSameRequest =>
"The transaction lost a race. Re-sending the identical request is safe.";
IdempotencyConflict: Transaction =>
"That idempotency key already names a different request. Use a new key, or re-send the \
original request bytes.";
TransactionUnknown: Transaction, OutcomeLookupRequired =>
"Look the transaction up by its idempotency key before assuming anything about it.";
OutcomeUnknown: Transaction, OutcomeLookupRequired =>
"Do not create a fresh mutation. Look the transaction up by idempotency key, or retry \
the exact same logical request with the same key.";
TransactionTooLarge: Transaction =>
"Split the mutation into smaller coherent transactions.";
HistoricalSnapshotUnavailable: History, RequiresNewSnapshot =>
"That history is no longer retained. Read at a newer coordinate.";
CursorMismatch: History =>
"The cursor belongs to a different query. Restart pagination.";
CursorTypeMismatch: History =>
"The cursor is for a different result kind. Restart pagination.";
CursorExpired: History, RequiresNewSnapshot =>
"Restart pagination from a fresh first page; a change cursor restarts from a sequence \
you recorded, never from the current head. `details.family` names the cursor family.";
CursorInvalid: History, RequiresNewSnapshot =>
"The cursor is malformed, belongs to another traversal, or was invalidated \
(`details.reason`: malformed, access_revoked, schema_changed). Restart pagination from \
a fresh first page.";
SearchModeUnsupported: Search =>
"Run `DESCRIBE CAPABILITIES` for the SEARCH modes this runtime offers.";
SearchIndexUnavailable: Search, SafeSameRequest =>
"The index is temporarily unavailable; the same request may succeed shortly.";
HistoricalSearchUnavailable: Search, NonRetryable =>
"Historical SEARCH is not supported here; read the current index instead.";
ArtifactUnavailable: Artifact, RequiresReacquireArtifact =>
"Re-upload or re-stage the artifact, then retry with the new handle.";
ArtifactTooLarge: Artifact =>
"The artifact exceeds this runtime's limit. Split it or reference it externally.";
ArtifactParseError: Artifact =>
"The bytes are not a well-formed artifact of the declared kind.";
DigestMismatch: Artifact, RequiresReacquireArtifact =>
"The bytes do not match the declared digest. Re-acquire the artifact.";
ProofInvalid: Artifact, NonRetryable =>
"The proof did not verify. Do not treat the artifact as trusted.";
SignerUnknown: Artifact, NonRetryable =>
"The signer is unknown here. Establish trust explicitly before importing.";
BlobUnavailable: Artifact, RequiresReacquireArtifact =>
"A referenced blob is missing. Re-acquire it, or import with a redaction-tolerant mode.";
CapsuleValidationFailed: Artifact =>
"Run `VALIDATE CAPSULE` to see exactly which invariant the Capsule breaks.";
ImportPreviewConflict: Artifact, RequiresRefresh =>
"The destination changed since the preview. Re-run `PREVIEW IMPORT CAPSULE` and retry.";
ResourceExhausted: Resource =>
"Reduce the request's cost: lower `LIMIT`, narrow the patterns, or paginate.";
ResultLimitExceeded: Resource =>
"Use `LIMIT` with `CURSOR` to page through the result set.";
ExecutionTimeout: Resource, OutcomeLookupRequired =>
"A deadline is not an abort: look the transaction up by idempotency key before \
deciding. For a read, simplify it — fewer UNION branches, a lower LIMIT, fewer path \
hops.";
RateLimited: Resource, SafeSameRequest =>
"Back off and retry the identical request.";
InternalError: System, OutcomeLookupRequired =>
"Retry under the same idempotency key; if it persists, report the `request_id`.";
}
impl Display for KipErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for KipErrorCode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
KipErrorCode::from_name(s).ok_or_else(|| format!("unknown KIP error code {s:?}"))
}
}
#[derive(Error, Debug, Clone, PartialEq)]
#[error("{code}: {message}")]
pub struct KipError {
pub code: KipErrorCode,
pub message: String,
pub hint: Option<String>,
pub details: Option<Json>,
}
impl KipError {
pub fn new(code: KipErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
hint: None,
details: None,
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_details(mut self, details: Json) -> Self {
self.details = Some(details);
self
}
pub fn name(&self) -> &'static str {
self.code.name()
}
pub fn category(&self) -> ErrorCategory {
self.code.category()
}
pub fn retry_class(&self) -> RetryClass {
self.code.retry_class()
}
pub fn effective_hint(&self) -> &str {
self.hint.as_deref().unwrap_or_else(|| self.code.hint())
}
pub fn cursor_expired(family: &str, message: impl Display) -> Self {
Self::new(KipErrorCode::CursorExpired, message.to_string())
.with_details(serde_json::json!({ "family": family, "reason": "expired" }))
}
pub fn cursor_invalid(family: &str, reason: &str, message: impl Display) -> Self {
Self::new(KipErrorCode::CursorInvalid, message.to_string())
.with_details(serde_json::json!({ "family": family, "reason": reason }))
}
pub fn invalid_lifecycle_transition_from(from: &str, to: &str, message: impl Display) -> Self {
Self::new(
KipErrorCode::InvalidLifecycleTransition,
message.to_string(),
)
.with_details(serde_json::json!({ "from": from, "to": to }))
}
pub fn version_conflict_on_plane(plane: &str, message: impl Display) -> Self {
Self::new(KipErrorCode::VersionConflict, message.to_string())
.with_details(serde_json::json!({ "plane": plane }))
}
}
macro_rules! kip_error_constructors {
($($fn_name:ident => $code:ident),* $(,)?) => {
impl KipError {
$(
#[doc = concat!("Creates a [`KipErrorCode::", stringify!($code), "`] error.")]
pub fn $fn_name(err: impl Display) -> Self {
Self::new(KipErrorCode::$code, format!("{err}"))
}
)*
}
};
}
kip_error_constructors! {
invalid_syntax => InvalidSyntax,
invalid_identifier => InvalidIdentifier,
invalid_request_envelope => InvalidRequestEnvelope,
unsupported_protocol_version => UnsupportedProtocolVersion,
unsupported_capability => UnsupportedCapability,
language_mismatch => LanguageMismatch,
readonly_violation => ReadonlyViolation,
duplicate_local_handle => DuplicateLocalHandle,
duplicate_mutation_target => DuplicateMutationTarget,
schema_symbol_not_found => SchemaSymbolNotFound,
schema_field_not_found => SchemaFieldNotFound,
type_mismatch => TypeMismatch,
constraint_violation => ConstraintViolation,
not_found_or_not_visible => NotFoundOrNotVisible,
reference_error => ReferenceError,
structural_reference_invalid => StructuralReferenceInvalid,
identity_selector_required => IdentitySelectorRequired,
name_identity_forbidden => NameIdentityForbidden,
client_key_conflict => ClientKeyConflict,
immutable_field => ImmutableField,
epistemic_revision_required => EpistemicRevisionRequired,
evidence_correction_required => EvidenceCorrectionRequired,
invalid_lifecycle_transition => InvalidLifecycleTransition,
activity_terminal => ActivityTerminal,
projection_target_unbound => ProjectionTargetUnbound,
projection_target_unbounded => ProjectionTargetUnbounded,
projection_not_authorized => ProjectionNotAuthorized,
retraction_not_authorized => RetractionNotAuthorized,
unauthenticated => Unauthenticated,
not_authorized => NotAuthorized,
requires_approval => RequiresApproval,
requires_stronger_authentication => RequiresStrongerAuthentication,
actor_binding_required => ActorBindingRequired,
protected_system_field => ProtectedSystemField,
protected_governance_field => ProtectedGovernanceField,
protected_schema_state => ProtectedSchemaState,
legal_hold_conflict => LegalHoldConflict,
purge_denied => PurgeDenied,
version_conflict => VersionConflict,
precondition_failed => PreconditionFailed,
outcome_unknown => OutcomeUnknown,
capsule_validation_failed => CapsuleValidationFailed,
resource_exhausted => ResourceExhausted,
result_limit_exceeded => ResultLimitExceeded,
execution_timeout => ExecutionTimeout,
internal_error => InternalError,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ErrorObject {
pub code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<ErrorCategory>,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry: Option<RetryInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<Json>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct RetryInfo {
pub class: RetryClass,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_ms: Option<u64>,
}
impl RetryInfo {
pub fn new(class: RetryClass) -> Self {
Self {
class,
after_ms: None,
}
}
pub fn after_ms(mut self, ms: u64) -> Self {
self.after_ms = Some(ms);
self
}
}
impl ErrorObject {
pub fn new(code: KipErrorCode, message: impl Into<String>) -> Self {
KipError::new(code, message).into()
}
pub fn parsed_code(&self) -> Option<KipErrorCode> {
KipErrorCode::from_name(&self.code)
}
}
impl From<KipError> for ErrorObject {
fn from(err: KipError) -> Self {
ErrorObject {
code: err.code.name().to_string(),
category: Some(err.code.category()),
message: err.message,
hint: Some(err.hint.unwrap_or_else(|| err.code.hint().to_string())),
retry: Some(RetryInfo::new(err.code.retry_class())),
details: err.details,
}
}
}
impl From<serde_json::Error> for ErrorObject {
fn from(err: serde_json::Error) -> Self {
ErrorObject::new(
KipErrorCode::InvalidRequestEnvelope,
format!("malformed JSON: {err}"),
)
}
}
impl From<serde_json::Error> for KipError {
fn from(err: serde_json::Error) -> Self {
KipError::invalid_request_envelope(format!("malformed JSON: {err}"))
}
}
impl Display for ErrorObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
pub(crate) fn format_nom_error(input: &str, err: nom::Err<VerboseError<&str>>) -> KipError {
let message = match err {
nom::Err::Incomplete(needed) => {
format!("parse incomplete, need more input: {needed:?}")
}
nom::Err::Error(ve) | nom::Err::Failure(ve) => format_verbose_error(input, ve),
};
KipError::invalid_syntax(message)
}
fn format_verbose_error(input: &str, ve: VerboseError<&str>) -> String {
let mut msg = String::new();
for (i, (substring, kind)) in ve.errors.iter().enumerate() {
let offset = input.len() - substring.len();
let (line, column) = line_column(input, offset);
let snippet = snippet_at(substring);
if i > 0 {
msg.push_str("\n ");
}
match kind {
VerboseErrorKind::Context(ctx) => {
msg.push_str(&format!("at line {line}, column {column}: expected {ctx}"));
}
VerboseErrorKind::Char(c) => {
msg.push_str(&format!("at line {line}, column {column}: expected {c:?}"));
}
VerboseErrorKind::Nom(e) => {
msg.push_str(&format!("at line {line}, column {column}: {e:?}"));
}
}
if !snippet.is_empty() {
msg.push_str(&format!(", found {snippet:?}"));
}
}
if msg.is_empty() {
"the input is not a valid KIP command".to_string()
} else {
msg
}
}
fn line_column(input: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(input.len());
let consumed = &input[..offset];
let line = consumed.matches('\n').count() + 1;
let column = match consumed.rfind('\n') {
Some(idx) => consumed[idx + 1..].chars().count() + 1,
None => consumed.chars().count() + 1,
};
(line, column)
}
fn snippet_at(remaining: &str) -> String {
const MAX: usize = 24;
let line = remaining.lines().next().unwrap_or("").trim_end();
if line.chars().count() <= MAX {
line.to_string()
} else {
let truncated: String = line.chars().take(MAX).collect();
format!("{truncated}…")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_registered_code_round_trips_by_name() {
for code in KipErrorCode::ALL {
assert_eq!(KipErrorCode::from_name(code.name()), Some(*code));
assert_eq!(
serde_json::to_string(code).unwrap(),
format!("\"{}\"", code.name())
);
}
}
#[test]
fn registry_covers_the_whole_spec_listing() {
assert_eq!(KipErrorCode::ALL.len(), 77);
let mut names: Vec<&str> = KipErrorCode::ALL.iter().map(|c| c.name()).collect();
names.sort_unstable();
let unique = names.len();
names.dedup();
assert_eq!(names.len(), unique, "duplicate code name in the registry");
}
#[test]
fn every_code_has_a_hint() {
for code in KipErrorCode::ALL {
assert!(!code.hint().is_empty(), "{code} has no hint");
}
}
#[test]
fn existence_neutral_error_stays_neutral() {
assert_eq!(
KipErrorCode::NotFoundOrNotVisible.category(),
ErrorCategory::Data
);
}
#[test]
fn error_object_carries_category_hint_and_retry() {
let obj: ErrorObject = KipError::version_conflict("element changed").into();
assert_eq!(obj.code, "VersionConflict");
assert_eq!(obj.category, Some(ErrorCategory::Transaction));
assert_eq!(obj.retry, Some(RetryInfo::new(RetryClass::RequiresRefresh)));
assert!(obj.hint.unwrap().contains("EXPECT VERSION"));
let json = serde_json::to_value(ErrorObject::new(
KipErrorCode::SchemaSymbolAmbiguous,
"two packages define `Drug`",
))
.unwrap();
assert_eq!(json["code"], "SchemaSymbolAmbiguous");
assert_eq!(json["category"], "schema");
assert_eq!(json["retry"]["class"], "requires_different_input");
}
#[test]
fn custom_hint_and_details_survive_conversion() {
let err = KipError::not_authorized("no `derive` permission")
.with_hint("ask the Space owner for `derive`")
.with_details(serde_json::json!({"permission": "derive"}));
assert_eq!(err.effective_hint(), "ask the Space owner for `derive`");
let obj: ErrorObject = err.into();
assert_eq!(
obj.hint.as_deref(),
Some("ask the Space owner for `derive`")
);
assert_eq!(obj.details.unwrap()["permission"], "derive");
}
#[test]
fn lost_write_recovery_is_not_a_fresh_mutation() {
for code in [
KipErrorCode::OutcomeUnknown,
KipErrorCode::TransactionUnknown,
KipErrorCode::ExecutionTimeout,
KipErrorCode::InternalError,
] {
assert_eq!(
code.retry_class(),
RetryClass::OutcomeLookupRequired,
"{code} must not tell a caller the write definitely did not land"
);
}
}
#[test]
fn safe_same_request_is_reserved_for_outcomes_that_are_actually_known() {
for code in KipErrorCode::ALL {
if code.retry_class() != RetryClass::SafeSameRequest {
continue;
}
assert!(
matches!(
code,
KipErrorCode::SerializationConflict
| KipErrorCode::SearchIndexUnavailable
| KipErrorCode::RateLimited
),
"{code} claims nothing durable happened; does it establish that?"
);
}
}
#[test]
fn line_column_counts_from_one() {
let input = "FIND(?x)\nWHERE {\n bad\n}";
assert_eq!(line_column(input, 0), (1, 1));
assert_eq!(line_column(input, 9), (2, 1));
assert_eq!(line_column(input, 19), (3, 3));
assert_eq!(line_column(input, 9_999).0, 4);
}
#[test]
fn snippet_truncates_on_char_boundaries() {
let long = "查询查询查询查询查询查询查询查询查询查询查询查询查询";
let snippet = snippet_at(long);
assert!(snippet.ends_with('…'));
assert_eq!(snippet.chars().count(), 25);
}
}