Files
windmill/cli/test/shared_ui_diff_unit.test.ts
T
Diego ImbertandClaude Opus 5 66123f3a9b feat: add --keep-deleted flag to wmill sync pull and push (#10878)
* feat: add --keep-deleted flag to wmill sync pull and push

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMfhFSZQJJRLPrfkug6VoK

* fix: address review findings on --keep-deleted

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMfhFSZQJJRLPrfkug6VoK

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 13:26:08 +00:00

150 lines
5.2 KiB
TypeScript

/**
* diffSharedUi must mirror pushSharedUi's apply semantics so the git-sync
* dry-run preview and the real apply never diverge: it emits ui/<rel> entries
* exactly when the local ui/ folder differs from the remote store, and nothing
* (including no local folder) when the apply would be a no-op.
*/
import { expect, test, describe, beforeEach, afterEach, mock } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
let remoteFiles: Record<string, string> = {};
let remoteUnreadable = false;
let pushedFiles: Record<string, string> | undefined;
mock.module("../gen/services.gen.ts", () => ({
getSharedUi: async (_args: { workspace: string }) => {
if (remoteUnreadable) throw new Error("shared UI store unreadable");
return { files: remoteFiles };
},
updateSharedUi: async (args: {
workspace: string;
requestBody: { files: Record<string, string> };
}) => {
pushedFiles = args.requestBody.files;
},
}));
const { diffSharedUi, pushSharedUi } = await import(
"../src/commands/shared_ui.ts"
);
describe("diffSharedUi", () => {
const ws = "test-workspace";
let tmpDir: string;
let prevCwd: string;
beforeEach(() => {
remoteFiles = {};
remoteUnreadable = false;
pushedFiles = undefined;
prevCwd = process.cwd();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wm-shared-ui-"));
process.chdir(tmpDir);
});
afterEach(() => {
process.chdir(prevCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writeUi(rel: string, content: string) {
const full = path.join(tmpDir, "ui", rel);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content, "utf-8");
}
test("emits an added ui/<rel> entry when local has a file the remote lacks", async () => {
writeUi("theme.json", "{}");
const changes = await diffSharedUi(ws);
expect(changes).toEqual([{ type: "added", path: "ui/theme.json" }]);
});
test("emits an edited ui/<rel> entry when local content differs", async () => {
remoteFiles = { "theme.json": "{}" };
writeUi("theme.json", '{"a":1}');
const changes = await diffSharedUi(ws);
expect(changes).toEqual([
{ type: "edited", path: "ui/theme.json", before: "{}", after: '{"a":1}' },
]);
});
test("emits a deleted ui/<rel> entry when remote has a file local lacks", async () => {
remoteFiles = { "theme.json": "{}", "extra.json": "1" };
writeUi("theme.json", "{}");
const changes = await diffSharedUi(ws);
expect(changes).toEqual([{ type: "deleted", path: "ui/extra.json" }]);
});
test("emits nothing when local matches the remote store", async () => {
remoteFiles = { "theme.json": "{}" };
writeUi("theme.json", "{}");
const changes = await diffSharedUi(ws);
expect(changes).toEqual([]);
});
test("diffs files named after Object.prototype members (e.g. toString)", async () => {
// Own-property check, not `in`: a local-only ui/toString is an add, and a
// remote-only ui/toString is a delete, despite Object.prototype.toString.
writeUi("toString", "x");
let changes = await diffSharedUi(ws);
expect(changes).toEqual([{ type: "added", path: "ui/toString" }]);
fs.rmSync(path.join(tmpDir, "ui", "toString"));
fs.mkdirSync(path.join(tmpDir, "ui"), { recursive: true });
remoteFiles = { toString: "x" };
changes = await diffSharedUi(ws);
expect(changes).toEqual([{ type: "deleted", path: "ui/toString" }]);
});
test("emits nothing when there is no local ui/ folder (apply is a no-op)", async () => {
remoteFiles = { "theme.json": "{}" };
const changes = await diffSharedUi(ws);
expect(changes).toEqual([]);
});
test("emits nothing under keepDeleted when the remote store is unreadable", async () => {
// pushSharedUi skips the push rather than clearing a store it can't read,
// so the preview must show that same nothing.
remoteUnreadable = true;
writeUi("theme.json", "{}");
expect(await diffSharedUi(ws, true)).toEqual([]);
});
});
describe("pushSharedUi with keepDeleted", () => {
const ws = "test-workspace";
let tmpDir: string;
let prevCwd: string;
beforeEach(() => {
remoteFiles = {};
remoteUnreadable = false;
pushedFiles = undefined;
prevCwd = process.cwd();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wm-shared-ui-push-"));
process.chdir(tmpDir);
});
afterEach(() => {
process.chdir(prevCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test("carries remote-only files, including ui/__proto__, into the pushed map", async () => {
// JSON.parse, not a literal: `{__proto__: …}` sets the prototype instead of
// creating the own property the API response really has.
remoteFiles = JSON.parse('{"__proto__":"keep me","extra.json":"1"}');
fs.mkdirSync(path.join(tmpDir, "ui"), { recursive: true });
fs.writeFileSync(path.join(tmpDir, "ui", "theme.json"), "{}", "utf-8");
expect(await pushSharedUi(ws, true)).toBe(true);
// The store is written whole, so anything missing here is deleted.
expect(pushedFiles!["extra.json"]).toEqual("1");
expect(Object.getOwnPropertyDescriptor(pushedFiles!, "__proto__")?.value)
.toEqual("keep me");
});
});