Compare commits

...
Author SHA1 Message Date
dev-guard 5d69b79ff6 test: scope membership and empty following selectors 2026-10-29 08:25:54 +00:00
2 changed files with 61 additions and 0 deletions

No files matched your search

@@ -83,3 +83,27 @@ describe("Core-event terminal following", () => {
expect(resumed.ok).toBe(true); expect(f.render).toHaveBeenCalledTimes(66);
});
});
describe("scope selector lifetime", () => {
it("keeps a known empty slice enabled and repopulates on a queue event", async () => {
vi.useFakeTimers(); const f = fixture(), service = f.create();
f.render.mockResolvedValueOnce({ ...answer(), opened: [], following: { ...answer().following!, state: "empty" } });
const result = await service.open({ ...selector, view: "slice:build" }, { enabled: true });
expect(result.following?.enabled).toBe(true);
expect(service.list()).toHaveLength(1);
f.emit("queue.created"); await vi.advanceTimersByTimeAsync(100);
expect(f.render).toHaveBeenCalledTimes(2);
expect(service.list()[0]?.status?.state).toBe("following");
});
it("restores scope selectors and coalesces explicit scope-source changes without polling", async () => {
vi.useFakeTimers(); const f = fixture(), first = f.create();
await first.open({ ...selector, view: "slice:build" }, { enabled: true }); await first.stop();
const second = f.create(); await vi.advanceTimersByTimeAsync(100);
expect(second.list()).toHaveLength(1);
const before = f.render.mock.calls.length;
for (let n = 0; n < 10; n++) f.emit("scope.sources_changed");
await vi.advanceTimersByTimeAsync(100); expect(f.render).toHaveBeenCalledTimes(before + 1);
await vi.advanceTimersByTimeAsync(600000); expect(f.render).toHaveBeenCalledTimes(before + 1);
});
});
@@ -739,3 +739,40 @@ describe("TerminalService — an omitted provider falls back from herdr to cmux
expect(cmux.lastView).toBeNull();
});
});
describe("slice task preview and explicit viewer capability", () => {
function sliceService(complete = true) {
const { deps, herdr } = makeDeps();
const snapshot = () => ({ task: { kind: "slice", id: "build", name: "Build it", complete,
issues: complete ? [] : ["Scope read unavailable"], members: [{ key: { hostId: "local", nodeId: "a" }, seat: "a@team",
obligations: [{ qitemId: "work", state: "pending" }], availability: "available", viewerStatus: "not-provisioned" }] },
members: [{ seat: "a@team", tmuxSession: "a", alive: true, readOnly: true }] });
Object.assign(deps, { resolveScopeView: () => snapshot() });
let effects = 0;
Object.assign(herdr, { ensureViewer: async () => { effects++; return { ok: false, code: "viewer_unconfirmed", error: "Fixture viewer not started" }; } });
return { service: new TerminalService(deps), effects: () => effects };
}
it("uses authoritative scoped membership instead of the live review band", async () => {
const { service } = sliceService();
const preview = await service.previewView({ view: "slice:build" });
expect(preview).toMatchObject({ task: { kind: "slice", id: "build", name: "Build it", complete: true,
members: [{ seat: "a@team", obligations: [{ qitemId: "work", state: "pending" }] }] }, capabilities: { ensureViewer: true } });
expect("task" in preview && preview.task).not.toHaveProperty("qitemId");
});
it("rejects changed scope plans before any owner ensure effect", async () => {
const { service, effects } = sliceService();
expect(await service.ensureViewer({ view: "slice:build", expectedPlan: "stale", member: { hostId: "local", nodeId: "a" } }))
.toMatchObject({ ok: false, code: "preview_changed" });
expect(effects()).toBe(0);
});
it("distinguishes an incomplete scope from an empty scope and never ensures it", async () => {
const { service, effects } = sliceService(false);
const preview = await service.previewView({ view: "slice:build" });
if (!("planId" in preview)) throw new Error("Expected scoped preview");
expect(preview.task?.complete).toBe(false);
expect(await service.ensureViewer({ view: "slice:build", expectedPlan: preview.planId, member: { hostId: "local", nodeId: "a" } }))
.toMatchObject({ ok: false, code: "viewer_unconfirmed" });
expect(effects()).toBe(0);
});
});