feat(sync): 添加 Vault 所有的用户 Skill 记录

This commit is contained in:
2026-09-09 08:39:14 +08:00
parent bdd1543a4d
commit b0783c9356
25 changed files with 1507 additions and 29 deletions
+145 -1
View File
@@ -368,5 +368,149 @@ async fn real_core_notes_roundtrip_only_through_bound_host_and_confirm_commits()
.unwrap()
.unwrap();
assert_eq!(stored["hash"], first["revision"]);
assert_eq!(workspace.lock().unwrap().pending_count().unwrap(), 9);
// User-created Skills are Vault records, use Host CAS/idempotency, and are
// resolved by the actual Agent route without copying package paths or grants.
let create_skill_operation = uuid::Uuid::new_v4();
let skill_body = json!({
"revision":"", "name":"Vault reviewer", "description":"portable",
"prompt":"Answer with the exact phrase user-skill-active.", "tools":[],
"permissions":[], "retrieval":{"top_k":10,"rerank":true,"citation":true},
"required_capabilities":["chat"]
});
let (status, user_skill) = request(
&mut core,
"POST",
"/api/user-skills",
&vault,
&create_skill_operation.to_string(),
Some(skill_body.clone()),
)
.await;
assert_eq!(status, 201, "{user_skill}");
let skill_id = user_skill["skill_id"].as_str().unwrap();
assert_eq!(
skill_id,
format!("user_skill_{}", create_skill_operation.simple())
);
assert_eq!(user_skill["status"], "ready");
for _ in 0..20 {
let (status, replay) = request(
&mut core,
"POST",
"/api/user-skills",
&vault,
&create_skill_operation.to_string(),
Some(skill_body.clone()),
)
.await;
assert_eq!(status, 201, "{replay}");
assert_eq!(replay, user_skill);
}
let mut changed = skill_body.clone();
changed["name"] = json!("Changed replay");
let (status, conflict) = request(
&mut core,
"POST",
"/api/user-skills",
&vault,
&create_skill_operation.to_string(),
Some(changed),
)
.await;
assert_eq!(status, 409, "{conflict}");
assert_eq!(conflict["error"]["code"], "USER_SKILL_OPERATION_CONFLICT");
let (status, listed) = request(
&mut core,
"GET",
"/api/user-skills?limit=100&offset=0",
&vault,
&uuid::Uuid::new_v4().to_string(),
None,
)
.await;
assert_eq!(status, 200, "{listed}");
assert_eq!(listed["items"][0]["skill_id"], skill_id);
let (status, denied) = request(
&mut core,
"GET",
"/api/user-skills?limit=100&offset=0",
&uuid::Uuid::new_v4().to_string(),
&uuid::Uuid::new_v4().to_string(),
None,
)
.await;
assert_eq!(status, 409, "{denied}");
assert_eq!(denied["error"]["code"], "VAULT_PERMISSION_CHANGED");
let mut update_body = skill_body.clone();
update_body["revision"] = user_skill["revision"].clone();
update_body["name"] = json!("Updated reviewer");
let update_operation = uuid::Uuid::new_v4().to_string();
let (status, updated_skill) = request(
&mut core,
"PUT",
&format!("/api/user-skills/{skill_id}"),
&vault,
&update_operation,
Some(update_body.clone()),
)
.await;
assert_eq!(status, 200, "{updated_skill}");
assert_eq!(updated_skill["data"]["version"], 2);
let (status, stale) = request(
&mut core,
"PUT",
&format!("/api/user-skills/{skill_id}"),
&vault,
&uuid::Uuid::new_v4().to_string(),
Some(update_body.clone()),
)
.await;
assert_eq!(status, 409, "{stale}");
assert_eq!(stale["error"]["code"], "USER_SKILL_REVISION_CONFLICT");
for _ in 0..2 {
let (status, replay) = request(
&mut core,
"PUT",
&format!("/api/user-skills/{skill_id}"),
&vault,
&update_operation,
Some(update_body.clone()),
)
.await;
assert_eq!(status, 200, "{replay}");
assert_eq!(replay, updated_skill);
}
let (status, run) = request(
&mut core,
"POST",
"/api/agent/runs",
&vault,
&uuid::Uuid::new_v4().to_string(),
Some(json!({"input":"Confirm the Skill configuration","provider_id":"mock","model":"mock-1","skill_id":skill_id})),
)
.await;
assert_eq!(status, 202, "{run}");
assert_eq!(run["skill_id"], skill_id);
let delete_operation = uuid::Uuid::new_v4().to_string();
let delete_path = format!(
"/api/user-skills/{skill_id}?revision={}",
updated_skill["revision"].as_str().unwrap()
);
for _ in 0..2 {
let (status, deleted) = request(
&mut core,
"DELETE",
&delete_path,
&vault,
&delete_operation,
None,
)
.await;
assert_eq!(status, 200, "{deleted}");
}
assert!(!root
.join(format!("opennexus-records/v1/user-skills/{skill_id}.json"))
.exists());
assert_eq!(workspace.lock().unwrap().pending_count().unwrap(), 12);
}
+24 -2
View File
@@ -566,6 +566,12 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
json!({"primaryExpanded":true,"workspaceWidth":272,"chatWidth":320}),
"workspaceWidth",
),
(
"user_skill",
"user_skill_00000000000000000000000000000001",
json!({"version":1,"name":"initial","description":"portable","prompt":"Review carefully","tools":["notes.read"],"permissions":["notes.read"],"retrieval":{"top_k":10,"rerank":true,"citation":true},"required_capabilities":["chat"],"created_at_ms":1,"updated_at_ms":1}),
"name",
),
] {
let path = notesagent_host::records::path_for(kind, id).unwrap();
let record = json!({"schema":1,"kind":kind,"id":id,"data":data});
@@ -582,7 +588,7 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
let mut ws = target.lock().unwrap();
let current = ws.record_get_kind(kind, id).unwrap().unwrap();
let mut next = current["record"].clone();
next["data"][field] = if kind == "persona" {
next["data"][field] = if kind != "layout" {
json!(format!("side-{side}-round-{round}"))
} else {
json!(300 + round * 2 + side)
@@ -665,7 +671,11 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
0
);
assert!(!client_b.push_one(&workspace_b, &binding_b).await.unwrap());
for (kind, id) in [("persona", "default"), ("layout", "sidebars")] {
for (kind, id) in [
("persona", "default"),
("layout", "sidebars"),
("user_skill", "user_skill_00000000000000000000000000000001"),
] {
assert_eq!(
workspace.lock().unwrap().record_get_kind(kind, id).unwrap(),
workspace_b
@@ -691,6 +701,18 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
.unwrap()
> 0
{}
assert_eq!(
excluded_ws
.lock()
.unwrap()
.record_get_kind("user_skill", "user_skill_00000000000000000000000000000001")
.unwrap(),
workspace
.lock()
.unwrap()
.record_get_kind("user_skill", "user_skill_00000000000000000000000000000001")
.unwrap()
);
for (kind, id) in [("persona", "default"), ("layout", "sidebars")] {
let original = workspace
.lock()