fix: rebuild ready snapshots on an explicit refresh (#890)

* fix repository refresh failing to reflect new public GitHub commits

* test: cover forced snapshot rebuilds and unchanged background refresh

* fix: serialize missing-tree recovery with owner refreshes

---------

Co-authored-by: Thomas Durieux <5577568+tdurieux@users.noreply.github.com>
This commit is contained in:
KozakLordOfMatrixandThomas Durieux authored and GitHub committed 2026-10-11 11:53:45 +01:00
1 parent bee729e0ee
commit 4a06aa9888
3 files changed
+75 -5

No files matched your search

+69
View File
@@ -129,6 +129,46 @@ describe("repository refresh and restoration", () => {
return stored;
}
for (const force of [false, true]) {
for (const commit of ["saved-sha", "new-sha"]) {
it(`refreshes a ready snapshot with force=${force}, commit=${commit}`, async () => {
const repo = repository("ready");
repo.model.options.update = true;
repo.model.lastView = new Date(0);
const stored = database(repo);
repo.getToken = async () => "token";
stub(github, "getRepositoryFromGitHub", async options => {
expect(options.force).to.equal(true);
return { fullName: "owner/repo", model: { defaultBranch: "main" },
branches: async options => {
expect(options.force).to.equal(true);
return [{ name: "main", commit }];
}, getCommitInfo: async () => ({ commit: {} }) };
});
let cleared = false, queued = false;
repo.resetSate = async () => {
expect(stored.source.commit).to.equal(commit);
expect(stored.status).to.equal("preparing");
cleared = true;
};
stub(queue, "downloadQueue", { getJob: async () => undefined, add: async (_id, data) => {
expect(cleared).to.equal(true);
expect(data.repoId).to.equal("restore-me");
queued = true;
} });
if (force) await repo.refresh();
else await repo.updateIfNeeded();
const rebuilt = force || commit !== "saved-sha";
expect(cleared).to.equal(rebuilt);
expect(queued).to.equal(rebuilt);
expect(stored.status).to.equal(rebuilt ? "preparing" : "ready");
expect(stored.source.commit).to.equal(commit);
expect(stored.repoId).to.equal("restore-me");
expect(stored).not.to.have.property("refreshToken");
});
}
}
it("claims a lease before GitHub work and rejects a concurrent refresh", async () => {
const first = repository("ready");
const second = new Repository(new Model(first.model.toObject()));
@@ -151,6 +191,35 @@ describe("repository refresh and restoration", () => {
expect(stored).not.to.have.property("refreshToken");
});
for (const recoveryFirst of [false, true]) {
it(`serializes missing-tree recovery and dashboard refresh (recovery first: ${recoveryFirst})`, async () => {
const first = repository("ready");
const second = new Repository(new Model(first.model.toObject()));
const stored = database(first);
const Files = require("../src/core/model/files/files.model").default;
stub(Files, "exists", () => ({ exec: async () => null }));
let release, started;
const entered = new Promise(resolve => { started = resolve; });
let updates = 0;
first.updateIfNeeded = async () => {
updates++;
started();
await new Promise(resolve => { release = resolve; });
};
second.updateIfNeeded = async () => { updates++; };
const running = recoveryFirst ? first.isReady() : first.refresh();
await entered;
try {
let failure;
try { await (recoveryFirst ? second.refresh() : second.isReady()); } catch (error) { failure = error; }
expect(failure?.message).to.equal("invalid_status");
expect(updates).to.equal(1);
expect(stored.status).to.equal("ready");
} finally { release(); await running; }
expect(stored).not.to.have.property("refreshToken");
});
}
it("releases a failed lease so reconnecting can be retried", async () => {
const repo = repository("removed");
const stored = database(repo);