fix: distinguish repository publication and settings dates

This commit is contained in:
Thomas Durieux
2026-09-13 08:42:54 +00:00
parent 22096a8346
commit 7cdf0f5e8d
12 changed files with 167 additions and 21 deletions
+96
View File
@@ -0,0 +1,96 @@
const { expect } = require("chai");
require("ts-node/register/transpile-only");
const db = require("../src/server/database");
const Model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const Files = require("../src/core/model/files/files.model").default;
const Repository = require("../src/core/Repository").default;
const utils = require("../src/server/routes/route-utils");
const queue = require("../src/queue");
function handler(name, path, method) {
return require(`../src/server/routes/${name}`).default.stack.find(layer =>
layer.route?.path === path && layer.route.methods[method]).route.stack[0].handle;
}
describe("repository activity dates", () => {
const restores = [];
function stub(object, key, value) {
const original = object[key]; restores.push(() => { object[key] = original; }); object[key] = value;
}
afterEach(() => { while (restores.length) restores.pop()(); });
function repository(status = "ready") {
return new Repository(new Model({ repoId: "activity", status,
owner: "507f1f77bcf86cd799439011", anonymizeDate: new Date("2026-01-02"),
source: { repositoryName: "owner/repo", commit: "abc123", branch: "main", commitDate: new Date("2020-01-01") },
options: { terms: [], expirationMode: "never" },
}));
}
function auth(repo) {
stub(utils, "getRepo", async () => repo);
stub(utils, "getUser", async () => ({ isAdmin: true }));
stub(utils, "handleError", error => { throw error; });
}
it("records readiness after a download, preserves it for progress messages, and advances it after restoration", async () => {
const repo = repository("download"); let saved;
stub(db, "isConnected", true);
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; return { matchedCount: 1 }; } }));
await repo.updateStatus("ready");
expect(saved.publishedAt).to.be.instanceOf(Date);
const first = repo.model.publishedAt;
await repo.updateStatus("ready", "progress");
expect(saved).not.to.have.property("publishedAt");
expect(repo.model.publishedAt).to.deep.equal(first);
await repo.updateStatus("expired");
await repo.updateStatus("preparing");
repo.model.publishedAt = new Date("2026-01-01");
await repo.updateStatus("ready");
expect(repo.model.publishedAt.getTime()).to.be.greaterThan(new Date("2026-01-01").getTime());
});
it("does not record publication when a concurrent removal cancels the worker", async () => {
const repo = repository("download"); repo.protectLifecycle = true;
stub(db, "isConnected", true);
stub(Model, "updateOne", () => ({ exec: async () => ({ matchedCount: 0 }) }));
try { await repo.updateStatus("ready"); throw new Error("expected cancellation"); }
catch (error) { expect(error.message).to.equal("repository_job_cancelled"); }
expect(repo.model.publishedAt).to.equal(undefined);
});
it("dates a settings-only save without rebuilding or changing the commit", async () => {
const repo = repository(); auth(repo); let saved;
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; return { matchedCount: 1 }; } }));
stub(queue, "downloadQueue", { add: async () => { throw new Error("must not rebuild"); } });
await handler("repository-private", "/:repoId/", "post")({ body: {
repoId: repo.repoId, fullName: "owner/repo", source: { commit: "abc123", branch: "main" },
terms: ["author"], options: { expirationMode: "never", image: false },
} }, { json: () => {} });
expect(saved.settingsSavedAt).to.be.instanceOf(Date);
expect(saved.source.commit).to.equal("abc123");
expect(saved).not.to.have.property("publishedAt");
});
it("dates a dashboard expiration extension", async () => {
const repo = repository(); auth(repo); let saved;
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; } }));
await handler("repository-private", "/:repoId/extend", "post")({}, { json: () => {} });
expect(saved.settingsSavedAt).to.be.instanceOf(Date);
expect(saved["options.expirationDate"]).to.be.instanceOf(Date);
});
it("returns publication and settings dates independently of the older source commit", async () => {
const repo = repository(); auth(repo);
repo.model.settingsSavedAt = new Date("2026-03-01");
repo.model.publishedAt = new Date("2026-02-01");
stub(Files, "exists", async () => null);
let body;
await handler("repository-public", "/:repoId/options", "get")({}, { header: () => {}, json: value => { body = value; } });
expect(body.lastUpdateDate).to.deep.equal(repo.model.settingsSavedAt);
expect(body.sourceCommitDate).to.deep.equal(new Date("2020-01-01"));
expect(body.publishedAt).to.deep.equal(repo.model.publishedAt);
});
it("does not invent publication or settings dates for legacy repositories", async () => {
const repo = repository(); auth(repo);
stub(Files, "exists", async () => null);
let body;
await handler("repository-public", "/:repoId/options", "get")({}, { header: () => {}, json: value => { body = value; } });
expect(body.publishedAt).to.equal(undefined);
expect(body.settingsSavedAt).to.equal(undefined);
expect(body.lastUpdateDate).to.deep.equal(repo.model.anonymizeDate);
});
});
+23
View File
@@ -341,6 +341,29 @@ describe("Vue 3 UI", function () {
expect(ui.errors).to.deep.equal([]);
});
it("distinguishes source, settings and readiness dates in the explorer", async function () {
ui = await browser("/r/test/hello.txt", {
"/api/repo/test/options": { anonymizedAt: "2026-01-01T00:00:00Z", sourceCommitDate: "2020-01-01T00:00:00Z",
settingsSavedAt: "2026-02-01T00:00:00Z", publishedAt: "2026-03-01T00:00:00Z" },
});
const footer = ui.window.document.querySelector(".leftCol-foot");
expect(footer.textContent).to.include("Source commit").and.include("Settings saved").and.include("Snapshot ready");
expect([...footer.querySelectorAll(".last-update")].map(node => node.title)).to.deep.equal([
"2020-01-01T00:00:00Z", "2026-02-01T00:00:00Z", "2026-03-01T00:00:00Z",
]);
expect(footer.textContent).not.to.include("not recorded");
expect(ui.errors).to.deep.equal([]);
});
it("identifies an unknown legacy publication date", async function () {
ui = await browser("/r/test/hello.txt", {
"/api/repo/test/options": { anonymizedAt: "2026-01-01T00:00:00Z", sourceCommitDate: "2020-01-01T00:00:00Z" },
});
const footer = ui.window.document.querySelector(".leftCol-foot");
expect(footer.textContent).to.include("Snapshot ready date not recorded");
expect(footer.textContent).not.to.include("Settings saved");
expect(ui.errors).to.deep.equal([]);
});
it("renders hostile filenames as text and updates the explorer without losing the tree", async function () {
const filename = '{{constructor.constructor("window.probe=1")()}}.txt';
ui = await browser("/r/test/hello.txt", {