Skip to content

Commit 5c29315

Browse files
Merge pull request #6 from matthiasdebernardini/chore/crates-io-dcosl-core
chore: switch dcosl-core to crates.io dependency
2 parents 48fb736 + 59ae803 commit 5c29315

10 files changed

Lines changed: 475 additions & 694 deletions

File tree

.config/nextest.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ max-threads = 1
44
[[profile.default.overrides]]
55
filter = "test(resolve_relay)"
66
test-group = "serial-env"
7+
8+
[[profile.default.overrides]]
9+
filter = "kind(test)"
10+
slow-timeout = { period = "30s", terminate-after = 2 }

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,46 @@ jobs:
9797
tool: cargo-nextest
9898

9999
- run: cargo nextest run
100+
101+
integration-test:
102+
name: Integration Test
103+
runs-on: ubuntu-latest
104+
permissions:
105+
contents: read
106+
services:
107+
strfry:
108+
image: ghcr.io/hoytech/strfry:latest
109+
ports:
110+
- 7777:7777
111+
steps:
112+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
113+
with:
114+
persist-credentials: false
115+
116+
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
117+
118+
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
119+
120+
- name: Install nextest
121+
uses: taiki-e/install-action@70e00552f3196d9a4c7dde7c57ef4c4830d422dd # v2.68.2
122+
with:
123+
tool: cargo-nextest
124+
125+
- name: Wait for strfry
126+
run: |
127+
for i in $(seq 1 30); do
128+
if curl -sf -o /dev/null http://localhost:7777 2>/dev/null || \
129+
curl -sf -o /dev/null -w '%{http_code}' http://localhost:7777 2>/dev/null | grep -qE '4[0-9]{2}'; then
130+
echo "strfry is ready"
131+
exit 0
132+
fi
133+
echo "Waiting for strfry... ($i/30)"
134+
sleep 1
135+
done
136+
echo "strfry failed to start"
137+
exit 1
138+
139+
- name: Run integration tests
140+
run: cargo nextest run --run-ignored ignored-only
141+
env:
142+
WOKHEI_RELAY: ws://localhost:7777

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ keywords = ["nostr", "decentralized", "cli", "agent", "dcosl"]
1313
categories = ["command-line-utilities"]
1414

1515
[dependencies]
16+
dcosl-core = "0.1.0"
1617
nostr-sdk = "0.44"
1718
agcli = { version = "0.6", features = ["jemalloc"] }
18-
tokio = { version = "1", features = ["rt-multi-thread"] }
19+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
1920
serde_json = "1"
2021
thiserror = "2"
2122
dirs = "6"

src/dtag.rs

Lines changed: 2 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,228 +1,2 @@
1-
use nostr_sdk::hashes::{Hash, sha256};
2-
3-
/// Compute a deterministic 8-hex-char suffix from a preimage string.
4-
fn suffix(preimage: &str) -> String {
5-
sha256::Hash::hash(preimage.as_bytes())
6-
.to_string()
7-
.chars()
8-
.take(8)
9-
.collect()
10-
}
11-
12-
/// Normalize a human string into a URL-safe slug (`[a-z0-9-]`).
13-
///
14-
/// Returns `fallback` if the input normalizes to empty.
15-
pub fn normalize(input: &str, fallback: &str) -> String {
16-
let slug: String = input
17-
.trim()
18-
.to_lowercase()
19-
.chars()
20-
.map(|c| if c.is_ascii_whitespace() { '-' } else { c })
21-
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-')
22-
.collect::<String>()
23-
.split('-')
24-
.filter(|s| !s.is_empty())
25-
.collect::<Vec<_>>()
26-
.join("-");
27-
28-
if slug.is_empty() {
29-
fallback.to_string()
30-
} else {
31-
slug
32-
}
33-
}
34-
35-
/// Generate a deterministic d-tag for a list header (kind 39998).
36-
///
37-
/// Format: `{slug}--{8-char-hex-suffix}`
38-
pub fn header_dtag(name_singular: &str, pubkey_hex: &str) -> String {
39-
let slug = normalize(name_singular, "list");
40-
let sfx = suffix(&format!("header|{pubkey_hex}|{slug}"));
41-
format!("{slug}--{sfx}")
42-
}
43-
44-
/// Generate a deterministic d-tag for a list item (kind 39999).
45-
///
46-
/// Format: `{slug}--{8-char-hex-suffix}`
47-
///
48-
/// The suffix is derived from the raw `anchor_value` (not the slug) to preserve
49-
/// sensitivity to the original input.
50-
pub fn item_dtag(parent_z: &str, anchor_value: &str) -> String {
51-
let slug = normalize(anchor_value, "item");
52-
let sfx = suffix(&format!("item|{parent_z}|{anchor_value}"));
53-
format!("{slug}--{sfx}")
54-
}
55-
56-
#[cfg(test)]
57-
mod tests {
58-
use super::*;
59-
60-
// -----------------------------------------------------------------------
61-
// normalize
62-
// -----------------------------------------------------------------------
63-
64-
#[test]
65-
fn normalize_simple_lowercase() {
66-
assert_eq!(normalize("Hello World", "x"), "hello-world");
67-
}
68-
69-
#[test]
70-
fn normalize_trims_whitespace() {
71-
assert_eq!(normalize(" spaced ", "x"), "spaced");
72-
}
73-
74-
#[test]
75-
fn normalize_collapses_whitespace_runs() {
76-
assert_eq!(normalize("a b c", "x"), "a-b-c");
77-
}
78-
79-
#[test]
80-
fn normalize_strips_special_chars() {
81-
assert_eq!(
82-
normalize("AI Agents! On @Nostr?", "x"),
83-
"ai-agents-on-nostr"
84-
);
85-
}
86-
87-
#[test]
88-
fn normalize_preserves_digits() {
89-
assert_eq!(normalize("Web3 Tools 42", "x"), "web3-tools-42");
90-
}
91-
92-
#[test]
93-
fn normalize_strips_unicode() {
94-
assert_eq!(normalize("café résumé", "x"), "caf-rsum");
95-
}
96-
97-
#[test]
98-
fn normalize_empty_input_returns_fallback() {
99-
assert_eq!(normalize("", "item"), "item");
100-
}
101-
102-
#[test]
103-
fn normalize_all_special_chars_returns_fallback() {
104-
assert_eq!(normalize("!@#$%^&*()", "list"), "list");
105-
}
106-
107-
#[test]
108-
fn normalize_only_whitespace_returns_fallback() {
109-
assert_eq!(normalize(" ", "list"), "list");
110-
}
111-
112-
#[test]
113-
fn normalize_leading_trailing_hyphens_trimmed() {
114-
assert_eq!(normalize("--hello--", "x"), "hello");
115-
}
116-
117-
#[test]
118-
fn normalize_repeated_hyphens_collapsed() {
119-
assert_eq!(normalize("a---b", "x"), "a-b");
120-
}
121-
122-
#[test]
123-
fn normalize_numeric_only() {
124-
assert_eq!(normalize("12345", "x"), "12345");
125-
}
126-
127-
// -----------------------------------------------------------------------
128-
// header_dtag
129-
// -----------------------------------------------------------------------
130-
131-
#[test]
132-
fn header_dtag_format() {
133-
let result = header_dtag("AI Agents on Nostr", "aabbccdd");
134-
assert!(result.starts_with("ai-agents-on-nostr--"));
135-
assert_eq!(result.len(), "ai-agents-on-nostr--".len() + 8);
136-
}
137-
138-
#[test]
139-
fn header_dtag_deterministic() {
140-
let a = header_dtag("test", "pubkey1");
141-
let b = header_dtag("test", "pubkey1");
142-
assert_eq!(a, b);
143-
}
144-
145-
#[test]
146-
fn header_dtag_different_names_differ() {
147-
let a = header_dtag("alpha", "pubkey1");
148-
let b = header_dtag("beta", "pubkey1");
149-
assert_ne!(a, b);
150-
}
151-
152-
#[test]
153-
fn header_dtag_different_pubkeys_differ() {
154-
let a = header_dtag("test", "pubkey1");
155-
let b = header_dtag("test", "pubkey2");
156-
assert_ne!(a, b);
157-
}
158-
159-
#[test]
160-
fn header_dtag_empty_name_uses_fallback() {
161-
let result = header_dtag("", "pubkey1");
162-
assert!(result.starts_with("list--"));
163-
}
164-
165-
// -----------------------------------------------------------------------
166-
// item_dtag
167-
// -----------------------------------------------------------------------
168-
169-
#[test]
170-
fn item_dtag_format() {
171-
let result = item_dtag("39998:pk:my-list", "https://example.com/resource");
172-
assert!(result.contains("--"));
173-
let parts: Vec<&str> = result.rsplitn(2, "--").collect();
174-
assert_eq!(parts[0].len(), 8); // suffix
175-
}
176-
177-
#[test]
178-
fn item_dtag_deterministic() {
179-
let a = item_dtag("parent-z", "https://example.com");
180-
let b = item_dtag("parent-z", "https://example.com");
181-
assert_eq!(a, b);
182-
}
183-
184-
#[test]
185-
fn item_dtag_different_parents_differ() {
186-
let a = item_dtag("parent-a", "https://example.com");
187-
let b = item_dtag("parent-b", "https://example.com");
188-
assert_ne!(a, b);
189-
}
190-
191-
#[test]
192-
fn item_dtag_different_anchors_differ() {
193-
let a = item_dtag("parent", "https://a.com");
194-
let b = item_dtag("parent", "https://b.com");
195-
assert_ne!(a, b);
196-
}
197-
198-
#[test]
199-
fn item_dtag_empty_anchor_uses_fallback() {
200-
let result = item_dtag("parent-z", "");
201-
assert!(result.starts_with("item--"));
202-
}
203-
204-
// -----------------------------------------------------------------------
205-
// suffix
206-
// -----------------------------------------------------------------------
207-
208-
#[test]
209-
fn suffix_length_is_8() {
210-
assert_eq!(suffix("anything").len(), 8);
211-
}
212-
213-
#[test]
214-
fn suffix_is_hex() {
215-
let s = suffix("test-input");
216-
assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
217-
}
218-
219-
#[test]
220-
fn suffix_deterministic() {
221-
assert_eq!(suffix("same"), suffix("same"));
222-
}
223-
224-
#[test]
225-
fn suffix_different_inputs_differ() {
226-
assert_ne!(suffix("alpha"), suffix("beta"));
227-
}
228-
}
1+
// Re-export from dcosl-core
2+
pub use dcosl_core::dtag::*;

0 commit comments

Comments
 (0)