mirror of
https://github.com/cline/cline.git
synced 2026-08-29 03:52:41 +08:00
fix(core): confirm the Hub record still describes the daemon just probed
Instance identity is read from discovery before the probe and build data comes back after it, so a daemon replaced between those two steps was described with its predecessor's identity - the replacement then satisfied the prior daemon's pending sighting and emitted the notification the consecutive-instance check exists to suppress. Re-read discovery after the probe and report nothing when the record no longer describes the same daemon. A Hub mid-swap is churn; the next check sees whatever it settles into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,53 @@ describe("checkManagedHubBuildMismatch", () => {
|
||||
await expect(checkManagedHubBuildMismatch()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
/**
|
||||
* Identity is read before the probe and build data after it, so a daemon
|
||||
* swapped in between would otherwise be reported under its predecessor's
|
||||
* identity.
|
||||
*/
|
||||
it("returns undefined when the hub is replaced while it is being probed", async () => {
|
||||
vi.stubEnv("CLINE_HUB_BUILD_EPOCH_MS", "1000");
|
||||
const records = [
|
||||
{ ...liveRecord, hubId: "hub-instance-a" },
|
||||
{ ...liveRecord, hubId: "hub-instance-b" },
|
||||
];
|
||||
let read = 0;
|
||||
vi.doMock("../discovery/workspace", () => ({
|
||||
resolveProductionHubOwnerContext: () => ({
|
||||
ownerId: "hub-test",
|
||||
discoveryPath: "/tmp/hub-watcher-discovery.json",
|
||||
}),
|
||||
resolveSharedHubOwnerContext: () => ({
|
||||
ownerId: "hub-test",
|
||||
discoveryPath: "/tmp/hub-watcher-discovery.json",
|
||||
}),
|
||||
}));
|
||||
vi.doMock("../discovery", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../discovery")>("../discovery");
|
||||
return {
|
||||
...actual,
|
||||
resolveHubBuildId: () => "current-build",
|
||||
// Second read within the same check sees the replacement.
|
||||
readHubDiscovery: vi.fn(async () => records[read++] ?? records[1]),
|
||||
probeHubServer: vi.fn(async () => ({
|
||||
protocolVersion: "v1",
|
||||
buildId: "old-build",
|
||||
buildEpochMs: 500,
|
||||
host: "127.0.0.1",
|
||||
port: 59999,
|
||||
url: "ws://127.0.0.1:59999/hub",
|
||||
})),
|
||||
};
|
||||
});
|
||||
const { checkManagedHubBuildMismatch } = await import(
|
||||
"./managed-hub-build-watcher"
|
||||
);
|
||||
|
||||
await expect(checkManagedHubBuildMismatch()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the recorded hub is unreachable", async () => {
|
||||
mockDiscovery({ record: liveRecord, probe: undefined });
|
||||
const { checkManagedHubBuildMismatch } = await import(
|
||||
|
||||
@@ -133,6 +133,23 @@ export async function checkManagedHubBuildMismatch(): Promise<
|
||||
if (!healthy?.url) {
|
||||
return undefined;
|
||||
}
|
||||
// Reading discovery and probing the Hub are separate steps, and instance
|
||||
// identity comes from the first while the build data comes from the second.
|
||||
// A daemon replaced between them would be described with its predecessor's
|
||||
// identity, which is exactly the confusion the caller's consecutive-instance
|
||||
// check exists to avoid. Confirm the record still describes the daemon just
|
||||
// probed, and report nothing this round when it does not - a Hub mid-swap is
|
||||
// churn, and the next check sees whatever it settles into.
|
||||
const recheck = await readHubDiscovery(owner.discoveryPath).catch(
|
||||
() => undefined,
|
||||
);
|
||||
if (
|
||||
!recheck?.url ||
|
||||
recheck.url !== record.url ||
|
||||
resolveHubInstanceId({}, recheck) !== resolveHubInstanceId({}, record)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const expectedBuildId = resolveHubBuildId();
|
||||
const compatibility = getManagedHubCompatibility(healthy, expectedBuildId);
|
||||
if (compatibility.compatible) {
|
||||
|
||||
Reference in New Issue
Block a user