mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
## Summary - expose project deletion when the current user directly owns the project or owns/manages its author agent - publish a viewer-signed, project-only tombstone after fetching the exact live head, then detect concurrent replacement - honor relay-authorized owner-signed tombstones in desktop project read models ## Testing - pre-push hook: desktop Biome, TypeScript, 5,358 desktop tests, Rust tests, Tauri checks, and file-size ratchet passed at `a36f7a4cf` - `cargo check -p buzz-test-client --tests` - relay E2E coverage added as ignored; not run against a live relay --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
141 lines
3.8 KiB
JavaScript
141 lines
3.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { deleteProject } from "./projectDeletion.ts";
|
|
|
|
const OWNER = "a".repeat(64);
|
|
const VIEWER = "b".repeat(64);
|
|
const PROJECT_ADDRESS = `30621:${OWNER}:platform`;
|
|
const project = {
|
|
createdAt: 50,
|
|
description: "",
|
|
dtag: "platform",
|
|
id: PROJECT_ADDRESS,
|
|
legacy: false,
|
|
name: "Platform",
|
|
owner: OWNER,
|
|
primaryRepositoryAddress: `30617:${OWNER}:repo`,
|
|
projectAddress: PROJECT_ADDRESS,
|
|
projectChannelId: null,
|
|
repositories: [],
|
|
repositoryAddresses: [`30617:${OWNER}:repo`],
|
|
status: "active",
|
|
};
|
|
|
|
function event(overrides = {}) {
|
|
return {
|
|
id: "1".repeat(64),
|
|
kind: 30621,
|
|
pubkey: OWNER,
|
|
created_at: 75,
|
|
content: "",
|
|
tags: [["d", "platform"]],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("deleteProject lets the relay authorize an agent owner and tombstones only the project", async () => {
|
|
const calls = [];
|
|
await deleteProject(project, {
|
|
fetchEvents: async (filter) => {
|
|
calls.push(["fetch", filter]);
|
|
return calls.filter(([type]) => type === "fetch").length === 1
|
|
? [event()]
|
|
: [];
|
|
},
|
|
nowSeconds: () => 74,
|
|
signEvent: async (template) => {
|
|
calls.push(["sign", template]);
|
|
return event({
|
|
pubkey: VIEWER,
|
|
kind: template.kind,
|
|
content: template.content,
|
|
created_at: template.createdAt,
|
|
tags: template.tags,
|
|
});
|
|
},
|
|
publishEvent: async (signed) => {
|
|
calls.push(["publish", signed]);
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls[0][1], {
|
|
kinds: [30621],
|
|
authors: [OWNER],
|
|
"#d": ["platform"],
|
|
limit: 1,
|
|
});
|
|
assert.deepEqual(calls[1][1].tags, [["a", PROJECT_ADDRESS]]);
|
|
assert.equal(calls[1][1].createdAt, 76);
|
|
assert.equal(calls[2][1].pubkey, VIEWER);
|
|
assert.deepEqual(calls[3][1], calls[0][1]);
|
|
assert.equal(calls[1][1].content, "Delete project Platform");
|
|
});
|
|
|
|
test("deleteProject builds a one-coordinate tombstone", async () => {
|
|
const calls = [];
|
|
await deleteProject(project, {
|
|
fetchEvents: async () => (calls.length === 0 ? [event()] : []),
|
|
signEvent: async (template) => {
|
|
calls.push(template);
|
|
return event({
|
|
kind: template.kind,
|
|
content: template.content,
|
|
created_at: template.createdAt,
|
|
tags: template.tags,
|
|
});
|
|
},
|
|
publishEvent: async () => {},
|
|
});
|
|
|
|
assert.equal(calls[0].kind, 5);
|
|
assert.deepEqual(calls[0].tags, [["a", PROJECT_ADDRESS]]);
|
|
});
|
|
|
|
test("deleteProject fails closed when the live project head is missing", async () => {
|
|
await assert.rejects(
|
|
deleteProject(project, { fetchEvents: async () => [] }),
|
|
/Could not find this project on the relay/,
|
|
);
|
|
});
|
|
|
|
test("deleteProject reports a concurrent replacement that survives", async () => {
|
|
let fetchCount = 0;
|
|
await assert.rejects(
|
|
deleteProject(project, {
|
|
fetchEvents: async () => {
|
|
fetchCount += 1;
|
|
return [event({ created_at: fetchCount === 1 ? 75 : 77 })];
|
|
},
|
|
nowSeconds: () => 74,
|
|
signEvent: async (template) =>
|
|
event({
|
|
pubkey: VIEWER,
|
|
kind: template.kind,
|
|
created_at: template.createdAt,
|
|
tags: template.tags,
|
|
}),
|
|
publishEvent: async () => {},
|
|
}),
|
|
/updated while it was being deleted/,
|
|
);
|
|
});
|
|
|
|
test("deleteProject reports uncertain outcome when publish acknowledgement is lost", async () => {
|
|
await assert.rejects(
|
|
deleteProject(project, {
|
|
fetchEvents: async () => [event()],
|
|
signEvent: async (template) =>
|
|
event({
|
|
kind: template.kind,
|
|
created_at: template.createdAt,
|
|
tags: template.tags,
|
|
}),
|
|
publishEvent: async (_event, timeoutMessage) => {
|
|
throw new Error(timeoutMessage);
|
|
},
|
|
}),
|
|
/Could not confirm whether the project was deleted\. Projects were refreshed\./,
|
|
);
|
|
});
|