FN-7534: fix branch-group completion for archived unlanded members
Branch-group completion no longer silently drops archived-but-unlanded members, which previously let genuinely-incomplete groups be flagged complete and promoted. - listTasksByBranchGroup now scans with includeArchived:true so archived members stay counted in the group's total instead of dropping out silently - ArchivedTaskEntry gains a persisted mergeDetails snapshot so an archived member that had already landed is still distinguished from one that never landed - store.ts archival paths (task->archive projection) now carry mergeDetails through so isBranchGroupMemberLanded keeps working post-archival - Added regression coverage in branch-group-store.test.ts and group-merge-coordinator.test.ts for archived-landed and archived-unlanded gating - Added changeset documenting the fix as a patch-level bug fix Files changed: .changeset/fn-7534-branch-group-archived-member.md | 7 + docs/dashboard-guide.md | 2 + packages/core/src/__tests__/branch-group-store.test.ts | 77 +++++++++++ packages/core/src/store.ts | 30 ++++- packages/core/src/types.ts | 11 ++ packages/engine/src/__tests__/group-merge-coordinator.test.ts | 148 ++++++++++++++++++++- 6 files changed, 273 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7534 Fusion-Task-Lineage: 510af857-ce08-49a0-a2a1-41b3ad473804 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7534-branch-group-archived-member.md
Normal file
7
.changeset/fn-7534-branch-group-archived-member.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Branch groups no longer report complete (or become promotable) when an unlanded member is archived.
|
||||||
|
category: fix
|
||||||
|
dev: listTasksByBranchGroup membership now scans with includeArchived:true so an archived-but-unlanded member stays counted in total instead of silently dropping out; mergeDetails is now persisted on ArchivedTaskEntry so an archived member that had already landed keeps counting as landed. evaluateBranchGroupCompletion / promoteBranchGroup gate correctly; merge-target-safety in isBranchGroupMemberLanded is unchanged.
|
||||||
@@ -1827,6 +1827,8 @@ The Group Task Modal shows shared branch name/status, member list (`taskId`, tit
|
|||||||
|
|
||||||
> **FN-7532:** a member only counts as "landed" once it merge-confirms onto its OWN group's branch via the branch-group-integration path (`mergeDetails.mergeTargetSource === "branch-group-integration"` and a matching `mergeTargetBranch`) — this is the same predicate the engine's promotion gate uses, so the checklist can never show "complete" when a real promotion would still be refused (or vice versa). The merge engine now stamps this attribution for every merge (previously only the legacy merge path did, so shared-group members merged through the current path were undercounted).
|
> **FN-7532:** a member only counts as "landed" once it merge-confirms onto its OWN group's branch via the branch-group-integration path (`mergeDetails.mergeTargetSource === "branch-group-integration"` and a matching `mergeTargetBranch`) — this is the same predicate the engine's promotion gate uses, so the checklist can never show "complete" when a real promotion would still be refused (or vice versa). The merge engine now stamps this attribution for every merge (previously only the legacy merge path did, so shared-group members merged through the current path were undercounted).
|
||||||
|
|
||||||
|
> **FN-7534:** archiving a member does NOT remove it from its branch group's completion count. An archived member that never landed stays in `total` as pending, so the checklist and the engine promotion gate (`promoteBranchGroup`) both keep reporting the group incomplete — archiving a stuck/abandoned member is not a way to force a group to "complete". An archived member that HAD already landed before archival keeps counting as landed (its merge-confirmation is frozen at archive time), so a group that was genuinely done before one of its members got archived does not regress into a permanent stuck state.
|
||||||
|
|
||||||
Both the modal and branch-group card are completion-gated: while members are still pending, they show progress only. PR / merge controls are only revealed after all members are landed into the shared branch. When auto-merge is off, promote/open-PR is explicit user action (no automatic push-to-origin behavior).
|
Both the modal and branch-group card are completion-gated: while members are still pending, they show progress only. PR / merge controls are only revealed after all members are landed into the shared branch. When auto-merge is off, promote/open-PR is explicit user action (no automatic push-to-origin behavior).
|
||||||
|
|
||||||
### CLI-onboarding backfill runbook
|
### CLI-onboarding backfill runbook
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { rm } from "node:fs/promises";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { TaskStore } from "../store.js";
|
import { TaskStore } from "../store.js";
|
||||||
|
import { isBranchGroupMemberLanded } from "../branch-group-completion.js";
|
||||||
|
|
||||||
function makeTmpDir(): string {
|
function makeTmpDir(): string {
|
||||||
return mkdtempSync(join(tmpdir(), "fusion-branch-group-test-"));
|
return mkdtempSync(join(tmpdir(), "fusion-branch-group-test-"));
|
||||||
@@ -363,4 +364,80 @@ describe("TaskStore branch groups", () => {
|
|||||||
expect(archivedTask.autoMerge).toBe(true);
|
expect(archivedTask.autoMerge).toBe(true);
|
||||||
expect(archivedTask.branchContext?.groupId).toBe(group.id);
|
expect(archivedTask.branchContext?.groupId).toBe(group.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:BranchGroupCompletion 2026-07-04-00:00:
|
||||||
|
* FN-7534: an unlanded member archived while still belonging to a branch group must
|
||||||
|
* NEVER silently drop out of `listTasksByBranchGroup`'s membership set — it previously
|
||||||
|
* vanished from `total` without any corresponding drop in `landed`, letting
|
||||||
|
* `isBranchGroupComplete`/`evaluateBranchGroupCompletion` flip a genuinely-incomplete
|
||||||
|
* group to `complete: true`. The fix scans with `includeArchived: true` so archived
|
||||||
|
* members stay counted, and `mergeDetails` is now persisted on the archive entry (it
|
||||||
|
* was previously dropped at the archive boundary) so an archived member that HAD landed
|
||||||
|
* before archival keeps counting as landed rather than regressing to "pending" forever.
|
||||||
|
*/
|
||||||
|
it("keeps an archived unlanded member in branch-group membership so completion cannot go true prematurely (FN-7534)", async () => {
|
||||||
|
const group = store.createBranchGroup({ sourceType: "planning", sourceId: "PS-archived-unlanded", branchName: "fn/archived-unlanded" });
|
||||||
|
|
||||||
|
const landedTask = await store.createTask({ description: "landed member" });
|
||||||
|
await store.setTaskBranchGroup(landedTask.id, group.id);
|
||||||
|
await store.updateTask(landedTask.id, {
|
||||||
|
mergeDetails: {
|
||||||
|
mergeConfirmed: true,
|
||||||
|
mergeTargetSource: "branch-group-integration",
|
||||||
|
mergeTargetBranch: group.branchName,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const abandonedTask = await store.createTask({ description: "unlanded member, later archived" });
|
||||||
|
await store.setTaskBranchGroup(abandonedTask.id, group.id);
|
||||||
|
await store.archiveTask(abandonedTask.id);
|
||||||
|
|
||||||
|
const members = await store.listTasksByBranchGroup(group.id);
|
||||||
|
expect(members.map((task) => task.id).sort()).toEqual([abandonedTask.id, landedTask.id].sort());
|
||||||
|
|
||||||
|
const archivedMember = members.find((task) => task.id === abandonedTask.id)!;
|
||||||
|
expect(archivedMember.column).toBe("archived");
|
||||||
|
expect(isBranchGroupMemberLanded(archivedMember, group)).toBe(false);
|
||||||
|
|
||||||
|
const landedMember = members.find((task) => task.id === landedTask.id)!;
|
||||||
|
expect(isBranchGroupMemberLanded(landedMember, group)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves mergeDetails on an archived member that had already landed before archival (FN-7534)", async () => {
|
||||||
|
const group = store.createBranchGroup({ sourceType: "planning", sourceId: "PS-archived-landed", branchName: "fn/archived-landed" });
|
||||||
|
|
||||||
|
const task = await store.createTask({ description: "landed then archived" });
|
||||||
|
await store.setTaskBranchGroup(task.id, group.id);
|
||||||
|
await store.updateTask(task.id, {
|
||||||
|
mergeDetails: {
|
||||||
|
mergeConfirmed: true,
|
||||||
|
mergeTargetSource: "branch-group-integration",
|
||||||
|
mergeTargetBranch: group.branchName,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
await store.archiveTask(task.id);
|
||||||
|
|
||||||
|
const members = await store.listTasksByBranchGroup(group.id);
|
||||||
|
expect(members).toHaveLength(1);
|
||||||
|
expect(members[0].column).toBe("archived");
|
||||||
|
expect(isBranchGroupMemberLanded(members[0], group)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still matches a legacy synthetic-groupId member into membership after it is archived (FN-7534)", async () => {
|
||||||
|
const group = store.createBranchGroup({ sourceType: "planning", sourceId: "PS-legacy-archived", branchName: "fn/legacy-archived" });
|
||||||
|
const legacyTask = await store.createTask({
|
||||||
|
description: "legacy member, later archived",
|
||||||
|
branchContext: { groupId: "planning:PS-legacy-archived", source: "planning", assignmentMode: "shared" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.archiveTask(legacyTask.id);
|
||||||
|
|
||||||
|
const members = await store.listTasksByBranchGroup(group.id);
|
||||||
|
expect(members.map((task) => task.id)).toEqual([legacyTask.id]);
|
||||||
|
expect(isBranchGroupMemberLanded(members[0], group)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2340,6 +2340,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
missionId: entry.missionId,
|
missionId: entry.missionId,
|
||||||
sliceId: entry.sliceId,
|
sliceId: entry.sliceId,
|
||||||
assigneeUserId: entry.assigneeUserId,
|
assigneeUserId: entry.assigneeUserId,
|
||||||
|
// FNXC:BranchGroupCompletion 2026-07-04-00:00: FN-7534 — restore the frozen
|
||||||
|
// mergeDetails snapshot so isBranchGroupMemberLanded can still tell an
|
||||||
|
// archived-but-landed member apart from one that never landed.
|
||||||
|
mergeDetails: entry.mergeDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2482,6 +2486,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
mergeRetries: task.mergeRetries,
|
mergeRetries: task.mergeRetries,
|
||||||
error: task.error,
|
error: task.error,
|
||||||
modifiedFiles: task.modifiedFiles,
|
modifiedFiles: task.modifiedFiles,
|
||||||
|
// FNXC:BranchGroupCompletion 2026-07-04-00:00: FN-7534 — persist mergeDetails on
|
||||||
|
// archival so an archived member that had already landed against its branch group
|
||||||
|
// keeps counting as landed (see ArchivedTaskEntry.mergeDetails doc comment).
|
||||||
|
mergeDetails: task.mergeDetails,
|
||||||
missionId: task.missionId,
|
missionId: task.missionId,
|
||||||
sliceId: task.sliceId,
|
sliceId: task.sliceId,
|
||||||
assigneeUserId: task.assigneeUserId,
|
assigneeUserId: task.assigneeUserId,
|
||||||
@@ -5638,7 +5646,27 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listTasksByBranchGroup(groupId: string): Promise<Task[]> {
|
async listTasksByBranchGroup(groupId: string): Promise<Task[]> {
|
||||||
const tasks = await this.listTasks({ includeArchived: false, slim: true });
|
/*
|
||||||
|
* FNXC:BranchGroupCompletion 2026-07-04-00:00:
|
||||||
|
* FN-7534: this scan previously used `includeArchived: false`, which silently
|
||||||
|
* dropped an archived-but-UNLANDED member from `total` with no corresponding
|
||||||
|
* drop from `landed` — `isBranchGroupComplete` / `evaluateBranchGroupCompletion`
|
||||||
|
* ("total>0 && every member landed") could then flip a genuinely-incomplete
|
||||||
|
* group to `complete: true`, making the engine promotion gate
|
||||||
|
* (`promoteBranchGroup`) eligible to promote unfinished work. Scanning with
|
||||||
|
* `includeArchived: true` keeps every member — archived or not — in the
|
||||||
|
* membership set; `isBranchGroupMemberLanded` (merge-target-safety unchanged)
|
||||||
|
* still governs whether each one counts as landed. An archived member that had
|
||||||
|
* already landed before archival is now told apart from one that never landed
|
||||||
|
* via the mergeDetails snapshot persisted on the archive entry (see
|
||||||
|
* ArchivedTaskEntry.mergeDetails), so this does not regress a
|
||||||
|
* previously-complete group into a permanent deadlock. This is the SOLE
|
||||||
|
* membership scan shared by the dashboard branch-groups route, the CLI
|
||||||
|
* `branch-group` command, and the engine group-merge coordinator
|
||||||
|
* (`promoteBranchGroup`) — fixing it here means all three inherit the
|
||||||
|
* correction without further changes.
|
||||||
|
*/
|
||||||
|
const tasks = await this.listTasks({ includeArchived: true, slim: true });
|
||||||
// Membership filter (incl. legacy synthetic-groupId fallback) is shared with
|
// Membership filter (incl. legacy synthetic-groupId fallback) is shared with
|
||||||
// the dashboard list route via `filterTasksByBranchGroup` so semantics can't
|
// the dashboard list route via `filterTasksByBranchGroup` so semantics can't
|
||||||
// drift between the two call sites (Fix #8/#9).
|
// drift between the two call sites (Fix #8/#9).
|
||||||
|
|||||||
@@ -4994,6 +4994,17 @@ export interface ArchivedTaskEntry {
|
|||||||
error?: string;
|
error?: string;
|
||||||
/** User assigned to review this task (used during review handoff) */
|
/** User assigned to review this task (used during review handoff) */
|
||||||
assigneeUserId?: string;
|
assigneeUserId?: string;
|
||||||
|
/**
|
||||||
|
* FNXC:BranchGroupCompletion 2026-07-04-00:00:
|
||||||
|
* FN-7534: frozen merge-confirmation snapshot, captured at archive time. Previously
|
||||||
|
* dropped entirely on archival, which meant a branch-group member that had already
|
||||||
|
* landed before being archived could never be told apart from one that never landed —
|
||||||
|
* both looked identical (mergeDetails undefined) to isBranchGroupMemberLanded once
|
||||||
|
* archived. Persisting it here lets an archived-but-already-landed member keep
|
||||||
|
* counting as landed for branch-group completion instead of regressing to "pending"
|
||||||
|
* and permanently deadlocking an otherwise-complete group.
|
||||||
|
*/
|
||||||
|
mergeDetails?: MergeDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Type of planning question presented to the user */
|
/** Type of planning question presented to the user */
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { rm } from "node:fs/promises";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
import { describe, expect, it, afterEach } from "vitest";
|
import { describe, expect, it, afterEach, beforeEach } from "vitest";
|
||||||
|
import { TaskStore } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
evaluateBranchGroupCompletion,
|
evaluateBranchGroupCompletion,
|
||||||
evaluateBranchGroupPromotion,
|
evaluateBranchGroupPromotion,
|
||||||
@@ -123,6 +124,52 @@ describe("evaluateBranchGroupCompletion", () => {
|
|||||||
expect(result.complete).toBe(false);
|
expect(result.complete).toBe(false);
|
||||||
expect(result.pendingMemberIds).toEqual(["FN-A"]);
|
expect(result.pendingMemberIds).toEqual(["FN-A"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:BranchGroupCompletion 2026-07-04-00:00:
|
||||||
|
* FN-7534: an archived member that never landed onto the group branch must still count
|
||||||
|
* as pending — it must NOT silently drop out of the membership set (see
|
||||||
|
* TaskStore.listTasksByBranchGroup, which now scans with includeArchived: true so this
|
||||||
|
* shape of member reaches the coordinator at all).
|
||||||
|
*/
|
||||||
|
it("does NOT count an archived member that never landed onto the group branch (FN-7534)", () => {
|
||||||
|
const result = evaluateBranchGroupCompletion({
|
||||||
|
members: [
|
||||||
|
landed("FN-A"),
|
||||||
|
{ id: "FN-B", column: "archived" as const } as any,
|
||||||
|
] as any,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.complete).toBe(false);
|
||||||
|
expect(result.landedMemberIds).toEqual(["FN-A"]);
|
||||||
|
expect(result.pendingMemberIds).toEqual(["FN-B"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still counts an archived member as landed once its mergeDetails snapshot is preserved (FN-7534)", () => {
|
||||||
|
const result = evaluateBranchGroupCompletion({
|
||||||
|
members: [
|
||||||
|
landed("FN-A"),
|
||||||
|
{
|
||||||
|
id: "FN-B",
|
||||||
|
column: "archived" as const,
|
||||||
|
mergeDetails: {
|
||||||
|
mergeConfirmed: true,
|
||||||
|
mergeTargetSource: "branch-group-integration",
|
||||||
|
mergeTargetBranch: branchName,
|
||||||
|
} as any,
|
||||||
|
} as any,
|
||||||
|
] as any,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
complete: true,
|
||||||
|
totalMembers: 2,
|
||||||
|
landedMemberIds: ["FN-A", "FN-B"],
|
||||||
|
pendingMemberIds: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("evaluateBranchGroupPromotion", () => {
|
describe("evaluateBranchGroupPromotion", () => {
|
||||||
@@ -338,6 +385,105 @@ describe("promoteBranchGroup", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:BranchGroupCompletion 2026-07-04-00:00:
|
||||||
|
* FN-7534: the completion gate is a first-class regression target, not just the display
|
||||||
|
* serializers (FN-5893). These tests wire a REAL TaskStore (not a hand-rolled fixture) so
|
||||||
|
* `promoteBranchGroup` exercises the actual `listTasksByBranchGroup` membership scan that
|
||||||
|
* previously silently dropped an archived-but-unlanded member from `total`.
|
||||||
|
*/
|
||||||
|
describe("promoteBranchGroup with a real TaskStore (FN-7534 archived-member regression)", () => {
|
||||||
|
function makeTmpDir(): string {
|
||||||
|
return mkdtempSync(join(tmpdir(), "fusion-branch-group-archive-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootDir: string;
|
||||||
|
let storeRootDir: string;
|
||||||
|
let globalDir: string;
|
||||||
|
let store: TaskStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
rootDir = makeRepo();
|
||||||
|
storeRootDir = makeTmpDir();
|
||||||
|
globalDir = join(storeRootDir, ".fusion-global");
|
||||||
|
store = new TaskStore(storeRootDir, globalDir);
|
||||||
|
await store.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns reason: incomplete when an archived member never landed onto the group branch", async () => {
|
||||||
|
const group = store.createBranchGroup({
|
||||||
|
sourceType: "planning",
|
||||||
|
sourceId: "PS-archived-gate",
|
||||||
|
branchName: "fusion/groups/archived-gate",
|
||||||
|
});
|
||||||
|
|
||||||
|
const landedTask = await store.createTask({ description: "landed member" });
|
||||||
|
await store.setTaskBranchGroup(landedTask.id, group.id);
|
||||||
|
await store.updateTask(landedTask.id, {
|
||||||
|
mergeDetails: {
|
||||||
|
mergeConfirmed: true,
|
||||||
|
mergeTargetSource: "branch-group-integration",
|
||||||
|
mergeTargetBranch: group.branchName,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const abandonedTask = await store.createTask({ description: "unlanded member, later archived" });
|
||||||
|
await store.setTaskBranchGroup(abandonedTask.id, group.id);
|
||||||
|
await store.archiveTask(abandonedTask.id);
|
||||||
|
|
||||||
|
const result = await promoteBranchGroup({
|
||||||
|
rootDir,
|
||||||
|
groupId: group.id,
|
||||||
|
settings: { autoMerge: true, globalPause: false, enginePaused: false, mergeStrategy: "direct", baseBranch: "main" },
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.reason).toBe("incomplete");
|
||||||
|
expect(result.promoted).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still promotes when the only archived member had already landed before archival", async () => {
|
||||||
|
const group = store.createBranchGroup({
|
||||||
|
sourceType: "planning",
|
||||||
|
sourceId: "PS-archived-landed-gate",
|
||||||
|
branchName: "fusion/groups/archived-landed-gate",
|
||||||
|
autoMerge: true,
|
||||||
|
});
|
||||||
|
execSync(`git checkout -b ${group.branchName}`, { cwd: rootDir });
|
||||||
|
execSync("echo promoted > group.txt", { cwd: rootDir, shell: "/bin/bash" });
|
||||||
|
execSync("git add group.txt && git commit -m group", { cwd: rootDir, shell: "/bin/bash" });
|
||||||
|
execSync("git checkout main", { cwd: rootDir });
|
||||||
|
|
||||||
|
const task = await store.createTask({ description: "landed then archived" });
|
||||||
|
await store.setTaskBranchGroup(task.id, group.id);
|
||||||
|
await store.updateTask(task.id, {
|
||||||
|
mergeDetails: {
|
||||||
|
mergeConfirmed: true,
|
||||||
|
mergeTargetSource: "branch-group-integration",
|
||||||
|
mergeTargetBranch: group.branchName,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
await store.archiveTask(task.id);
|
||||||
|
|
||||||
|
const result = await promoteBranchGroup({
|
||||||
|
rootDir,
|
||||||
|
groupId: group.id,
|
||||||
|
settings: { autoMerge: true, globalPause: false, enginePaused: false, mergeStrategy: "direct", baseBranch: "main" },
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.reason).toBe("promoted");
|
||||||
|
expect(result.promoted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("promoteBranchGroup PR creation (U5)", () => {
|
describe("promoteBranchGroup PR creation (U5)", () => {
|
||||||
function makeGroup(overrides?: Partial<any>): any {
|
function makeGroup(overrides?: Partial<any>): any {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user