fix: preserve uncertain update outcomes

This commit is contained in:
Shakker
2026-07-29 22:08:41 +01:00
committed by Shakker
parent 8d47c7d274
commit f8aa44dcda
2 changed files with 49 additions and 10 deletions

View File

@@ -127,6 +127,18 @@ describe("application update reconciliation races", () => {
const overlays = createApplicationOverlays(harness.gateway);
try {
harness.update({
hello: {
server: { version: "1.0.0" },
snapshot: {
updateAvailable: {
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
},
},
} as ApplicationGatewaySnapshot["hello"],
});
const running = overlays.runUpdate();
await flushMicrotasks();
harness.update({ phase: "stopped" });
@@ -154,9 +166,16 @@ describe("application update reconciliation races", () => {
it("releases ambiguous reconciliation when update status access is revoked", async () => {
installUpdateTranslations();
const updateRun = deferred();
const request = vi.fn<RequestFn>((method) =>
method === "update.run" ? updateRun.promise : Promise.resolve([]),
);
const updateStatus = deferred();
const request = vi.fn<RequestFn>((method) => {
if (method === "update.run") {
return updateRun.promise;
}
if (method === "update.status") {
return updateStatus.promise;
}
return Promise.resolve([]);
});
const harness = createGatewayHarness(client(request));
harness.update({
hello: {
@@ -170,6 +189,11 @@ describe("application update reconciliation races", () => {
await flushMicrotasks();
expect(overlays.snapshot.updateReconciliationPending).toBe(true);
harness.update({ phase: "stopped" });
harness.update({ phase: "connected" });
await flushMicrotasks();
expect(request).toHaveBeenCalledWith("update.status", {});
harness.update({
hello: {
auth: { role: "operator", scopes: ["operator.read"] },
@@ -183,10 +207,24 @@ describe("application update reconciliation races", () => {
text: UNKNOWN_OUTCOME_TEXT,
});
updateStatus.resolve({
sentinel: {
kind: "update",
status: "error",
stats: { reason: "stale-result" },
},
});
await flushMicrotasks();
expect(overlays.snapshot.updateStatusBanner).toEqual({
tone: "danger",
text: UNKNOWN_OUTCOME_TEXT,
});
expect(request.mock.calls.filter(([method]) => method === "update.status")).toHaveLength(1);
updateRun.resolve({});
await running;
expect(request).not.toHaveBeenCalledWith("update.status", {});
} finally {
updateStatus.resolve({});
updateRun.resolve({});
overlays.dispose();
}

View File

@@ -342,12 +342,12 @@ export function createApplicationOverlays(
const currentVersion = gateway.snapshot.hello?.server?.version?.trim() || null;
pendingUpdate = null;
publishUpdateBanner(
expectedVersion && currentVersion !== expectedVersion
? resolveUpdateVerificationBanner({ expectedVersion, actualVersion: currentVersion })
: reconciliation.kind === "handoff"
? resolvePendingUpdateHandoffTimeoutBanner()
: reconciliation.kind === "ambiguous"
? unknownUpdateBanner()
reconciliation.kind === "ambiguous"
? unknownUpdateBanner()
: expectedVersion && currentVersion !== expectedVersion
? resolveUpdateVerificationBanner({ expectedVersion, actualVersion: currentVersion })
: reconciliation.kind === "handoff"
? resolvePendingUpdateHandoffTimeoutBanner()
: null,
);
};
@@ -378,6 +378,7 @@ export function createApplicationOverlays(
pairingPendingCount.invalidate({ clear: true });
if (accessTransition.adminRevoked) {
updateRunGeneration += 1;
cancelUpdateVerification();
const updateStatusBanner = pendingUpdate
? unknownUpdateBanner()
: snapshot.updateStatusBanner;