Files
buzz/desktop/src/features/projects/deleteProject.test.mjs
Wes 9176e34405 fix(projects): allow owners to delete agent projects (#6533)
## 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>
2026-08-27 09:33:04 -06:00

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\./,
);
});