Disconnect daemons locally when Hub is unreachable (#3321)
* fix(hub): make daemon disconnect unilateral Local relationship removal must not depend on reaching the Hub. Fence execution authority, make one bounded courtesy revocation attempt, then clear local state so status and reconnect eligibility agree immediately. * fix(hub): preserve disconnect intent across crashes Persist the terminal disconnect marker only while the bounded Hub notification is in flight. Startup removes that marker, so a crash cannot restore the old relationship or create a revocation retry loop.
This commit is contained in:
@@ -344,7 +344,6 @@ describe("Hub commands", () => {
|
||||
daemon.beforeDisconnect = () => {
|
||||
assert.equal(credentials.active()?.credential, "human-secret");
|
||||
};
|
||||
|
||||
const result = await runHubLogout(
|
||||
{ json: true, disconnectDaemon: true, force: true },
|
||||
{
|
||||
@@ -359,9 +358,31 @@ describe("Hub commands", () => {
|
||||
assert.equal(daemon.disconnects, 1);
|
||||
assert.deepEqual(daemon.disconnectForces, [true]);
|
||||
assert.equal(result.data.daemonDisconnected, true);
|
||||
assert.equal(result.data.warning, undefined);
|
||||
});
|
||||
|
||||
it("disconnect reports the current Hub and preserves it in the final result", async () => {
|
||||
it("logout reports a daemon disconnect warning", async () => {
|
||||
const credentials = new MemoryCredentials();
|
||||
credentials.save({ origin: "https://hub.test", credential: "human-secret" });
|
||||
const daemon = new FakeDaemon("https://hub.test");
|
||||
daemon.disconnectWarning = "Hub could not be reached; cleanup may remain pending.";
|
||||
|
||||
const result = await runHubLogout(
|
||||
{ json: true, disconnectDaemon: true },
|
||||
{
|
||||
credentials,
|
||||
daemon: new FakeDaemonConnection(daemon),
|
||||
isInteractive: () => false,
|
||||
confirmDisconnect: async () => false,
|
||||
reporter: quietReporter,
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.data.daemonDisconnected, true);
|
||||
assert.equal(result.data.warning, "Hub could not be reached; cleanup may remain pending.");
|
||||
});
|
||||
|
||||
it("disconnect reports clean local status without a ghost Hub origin", async () => {
|
||||
const daemon = new FakeDaemon("https://hub.test");
|
||||
const progress: string[] = [];
|
||||
|
||||
@@ -374,7 +395,7 @@ describe("Hub commands", () => {
|
||||
);
|
||||
|
||||
assert.deepEqual(progress, ["Disconnecting this daemon from https://hub.test"]);
|
||||
assert.equal(result.data[0]?.hub, "https://hub.test");
|
||||
assert.equal(result.data[0]?.hub, null);
|
||||
});
|
||||
|
||||
it("preserves login when an accepted daemon disconnect fails", async () => {
|
||||
@@ -448,6 +469,7 @@ class FakeDaemon implements HubDaemonClient {
|
||||
disconnects = 0;
|
||||
readonly disconnectForces: boolean[] = [];
|
||||
disconnectError: Error | null = null;
|
||||
disconnectWarning: string | null = null;
|
||||
beforeDisconnect: (() => void) | null = null;
|
||||
|
||||
constructor(private readonly origin: string) {}
|
||||
@@ -466,7 +488,10 @@ class FakeDaemon implements HubDaemonClient {
|
||||
this.disconnectForces.push(force);
|
||||
this.beforeDisconnect?.();
|
||||
if (this.disconnectError !== null) throw this.disconnectError;
|
||||
return { status: hubStatus("not_connected", null) };
|
||||
return {
|
||||
status: hubStatus("not_connected", null),
|
||||
...(this.disconnectWarning ? { warning: this.disconnectWarning } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async close() {}
|
||||
|
||||
Reference in New Issue
Block a user