mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): align workstream card identity schema
Signed-off-by: loganj <loganj@squareup.com> Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -14,8 +14,11 @@ function withCardFence(prose, rawPayload) {
|
||||
const VALID_PAYLOAD = {
|
||||
version: 1,
|
||||
synopsis: "Implementing the canvas card slice.",
|
||||
orchestrator: "loganj",
|
||||
assignees: ["alice", "bob"],
|
||||
orchestrator: { pubkey: "loganj-pubkey", name: "Logan" },
|
||||
assignees: [
|
||||
{ pubkey: "alice-pubkey", name: "Alice" },
|
||||
{ pubkey: "bob-pubkey", name: "Bob" },
|
||||
],
|
||||
};
|
||||
|
||||
// ── Happy path ────────────────────────────────────────────────────────────────
|
||||
@@ -34,7 +37,7 @@ test("parses a valid v1 card with explicit optional arrays", () => {
|
||||
version: 1,
|
||||
synopsis: VALID_PAYLOAD.synopsis,
|
||||
orchestrator: VALID_PAYLOAD.orchestrator,
|
||||
assignees: ["alice", "bob"],
|
||||
assignees: VALID_PAYLOAD.assignees,
|
||||
pullRequests: ["https://github.com/block/buzz/pull/1"],
|
||||
waitingOn: ["review"],
|
||||
});
|
||||
@@ -46,7 +49,7 @@ test("defaults assignees/pullRequests/waitingOn to empty arrays when omitted", (
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(result.card.assignees, ["alice", "bob"]);
|
||||
assert.deepEqual(result.card.assignees, VALID_PAYLOAD.assignees);
|
||||
assert.deepEqual(result.card.pullRequests, []);
|
||||
assert.deepEqual(result.card.waitingOn, []);
|
||||
});
|
||||
@@ -144,10 +147,10 @@ test("returns invalid-fields when synopsis is missing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("returns invalid-fields when orchestrator is an empty string", () => {
|
||||
test("returns invalid-fields when orchestrator is missing its identity fields", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
orchestrator: "",
|
||||
orchestrator: { name: "Logan" },
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
@@ -166,10 +169,10 @@ test("returns invalid-fields when assignees is not an array", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("returns invalid-fields when assignees contains a non-string", () => {
|
||||
test("returns invalid-fields when assignees contains an invalid identity", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
assignees: ["alice", 2],
|
||||
assignees: [{ pubkey: "alice-pubkey", name: "Alice" }, "bob"],
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
@@ -199,6 +202,28 @@ test("returns invalid-fields when waitingOn is not an array", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects the former string identity schema for orchestrator", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
orchestrator: "loganj",
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
reason: "invalid-fields",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects the former string identity schema for assignees", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
assignees: ["alice"],
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
reason: "invalid-fields",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns invalid-fields when the payload is a JSON array, not an object", () => {
|
||||
const content = withCardFence("Status:", [VALID_PAYLOAD]);
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
@@ -206,3 +231,25 @@ test("returns invalid-fields when the payload is a JSON array, not an object", (
|
||||
reason: "invalid-fields",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns invalid-fields when synopsis spans multiple lines", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
synopsis: "line one\nline two",
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
reason: "invalid-fields",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns invalid-fields when optional arrays are explicitly null", () => {
|
||||
const content = withCardFence("Status:", {
|
||||
...VALID_PAYLOAD,
|
||||
pullRequests: null,
|
||||
});
|
||||
assert.deepEqual(parseWorkstreamCard(content), {
|
||||
ok: false,
|
||||
reason: "invalid-fields",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* ```
|
||||
* ```buzz-workstream-card
|
||||
* {"version":1,"synopsis":"…","orchestrator":"…","assignees":[…]}
|
||||
* {"version":1,"synopsis":"…","orchestrator":{"pubkey":"…","name":"…"},"assignees":[{"pubkey":"…","name":"…"}]}
|
||||
* ```
|
||||
* ```
|
||||
*
|
||||
@@ -19,11 +19,16 @@
|
||||
const FENCE_OPEN = "```buzz-workstream-card";
|
||||
const FENCE_CLOSE = "```";
|
||||
|
||||
export type WorkstreamIdentity = {
|
||||
pubkey: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type WorkstreamCardV1 = {
|
||||
version: 1;
|
||||
synopsis: string;
|
||||
orchestrator: string;
|
||||
assignees: string[];
|
||||
orchestrator: WorkstreamIdentity;
|
||||
assignees: WorkstreamIdentity[];
|
||||
pullRequests: unknown[];
|
||||
waitingOn: unknown[];
|
||||
};
|
||||
@@ -40,30 +45,51 @@ export type WorkstreamCardParseResult =
|
||||
| { ok: false; reason: WorkstreamCardParseFailureReason };
|
||||
|
||||
function findFencedBlocks(content: string): string[] {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const blocks: string[] = [];
|
||||
let cursor = 0;
|
||||
|
||||
while (true) {
|
||||
const openIdx = content.indexOf(FENCE_OPEN, cursor);
|
||||
if (openIdx === -1) break;
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
if (lines[index].trimEnd() !== FENCE_OPEN) continue;
|
||||
|
||||
const jsonStart = content.indexOf("\n", openIdx);
|
||||
if (jsonStart === -1) break;
|
||||
const body: string[] = [];
|
||||
let closeIndex = index + 1;
|
||||
while (
|
||||
closeIndex < lines.length &&
|
||||
lines[closeIndex].trim() !== FENCE_CLOSE
|
||||
) {
|
||||
body.push(lines[closeIndex]);
|
||||
closeIndex += 1;
|
||||
}
|
||||
if (closeIndex === lines.length) break;
|
||||
|
||||
const closeIdx = content.indexOf(`\n${FENCE_CLOSE}`, jsonStart);
|
||||
if (closeIdx === -1) break;
|
||||
|
||||
blocks.push(content.slice(jsonStart + 1, closeIdx).trim());
|
||||
cursor = closeIdx + `\n${FENCE_CLOSE}`.length;
|
||||
blocks.push(body.join("\n").trim());
|
||||
index = closeIndex;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return (
|
||||
Array.isArray(value) && value.every((item) => typeof item === "string")
|
||||
);
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim() !== "";
|
||||
}
|
||||
|
||||
function isOneLineString(value: unknown): value is string {
|
||||
return isNonEmptyString(value) && !/[\r\n]/.test(value);
|
||||
}
|
||||
|
||||
function isWorkstreamIdentity(value: unknown): value is WorkstreamIdentity {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const raw = value as Record<string, unknown>;
|
||||
return isNonEmptyString(raw.pubkey) && isNonEmptyString(raw.name);
|
||||
}
|
||||
|
||||
function isWorkstreamIdentityArray(
|
||||
value: unknown,
|
||||
): value is WorkstreamIdentity[] {
|
||||
return Array.isArray(value) && value.every(isWorkstreamIdentity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,24 +128,27 @@ export function parseWorkstreamCard(
|
||||
return { ok: false, reason: "unknown-version" };
|
||||
}
|
||||
|
||||
if (typeof raw.synopsis !== "string" || raw.synopsis.trim() === "") {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
if (typeof raw.orchestrator !== "string" || raw.orchestrator.trim() === "") {
|
||||
const synopsis = raw.synopsis;
|
||||
if (!isOneLineString(synopsis)) {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
|
||||
const assignees = raw.assignees ?? [];
|
||||
if (!isStringArray(assignees)) {
|
||||
const orchestrator = raw.orchestrator;
|
||||
if (!isWorkstreamIdentity(orchestrator)) {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
|
||||
const pullRequests = raw.pullRequests ?? [];
|
||||
const assignees = raw.assignees === undefined ? [] : raw.assignees;
|
||||
if (!isWorkstreamIdentityArray(assignees)) {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
|
||||
const pullRequests = raw.pullRequests === undefined ? [] : raw.pullRequests;
|
||||
if (!Array.isArray(pullRequests)) {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
|
||||
const waitingOn = raw.waitingOn ?? [];
|
||||
const waitingOn = raw.waitingOn === undefined ? [] : raw.waitingOn;
|
||||
if (!Array.isArray(waitingOn)) {
|
||||
return { ok: false, reason: "invalid-fields" };
|
||||
}
|
||||
@@ -128,8 +157,8 @@ export function parseWorkstreamCard(
|
||||
ok: true,
|
||||
card: {
|
||||
version: 1,
|
||||
synopsis: raw.synopsis,
|
||||
orchestrator: raw.orchestrator,
|
||||
synopsis,
|
||||
orchestrator,
|
||||
assignees,
|
||||
pullRequests,
|
||||
waitingOn,
|
||||
|
||||
@@ -8,8 +8,8 @@ const VALID_CARD_CONTENT = [
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
synopsis: "Shipping the canvas card slice.",
|
||||
orchestrator: "loganj",
|
||||
assignees: ["alice"],
|
||||
orchestrator: { pubkey: "loganj-pubkey", name: "Logan" },
|
||||
assignees: [{ pubkey: "alice-pubkey", name: "Alice" }],
|
||||
}),
|
||||
"```",
|
||||
].join("\n");
|
||||
@@ -72,8 +72,13 @@ test("returns a ready card when the canvas parses successfully", () => {
|
||||
|
||||
assert.equal(viewModel.status, "ready");
|
||||
assert.equal(viewModel.card.synopsis, "Shipping the canvas card slice.");
|
||||
assert.equal(viewModel.card.orchestrator, "loganj");
|
||||
assert.deepEqual(viewModel.card.assignees, ["alice"]);
|
||||
assert.deepEqual(viewModel.card.orchestrator, {
|
||||
pubkey: "loganj-pubkey",
|
||||
name: "Logan",
|
||||
});
|
||||
assert.deepEqual(viewModel.card.assignees, [
|
||||
{ pubkey: "alice-pubkey", name: "Alice" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("loading takes priority over content even if content happens to be malformed", () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function WorkstreamCard({ channel, onSelect }: WorkstreamCardProps) {
|
||||
<p className="truncate text-2xs text-muted-foreground">
|
||||
Orchestrator:{" "}
|
||||
<span className="text-foreground">
|
||||
{viewModel.card.orchestrator}
|
||||
{viewModel.card.orchestrator.name}
|
||||
</span>
|
||||
</p>
|
||||
{viewModel.card.assignees.length > 0 ? (
|
||||
@@ -56,9 +56,9 @@ export function WorkstreamCard({ channel, onSelect }: WorkstreamCardProps) {
|
||||
{viewModel.card.assignees.map((assignee) => (
|
||||
<span
|
||||
className="rounded-full border border-border/65 bg-background/80 px-2 py-0.5 text-2xs text-muted-foreground"
|
||||
key={assignee}
|
||||
key={assignee.pubkey}
|
||||
>
|
||||
{assignee}
|
||||
{assignee.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user