Files
windmill/cli/test/pending_script_lock_unit.test.ts
Ruben Fiszel 6b916ac688 fix(cli): preserve committed script.lock on transient NULL lock during git-sync deploy (#9593)
* fix(cli): preserve committed script.lock on transient NULL lock during git-sync deploy (#9588)

A script's `lock` is NULL on the server only while a relock is mid-flight
(an importer relock after a relative-import dependency changed, or the
script's own first lock job). The git-sync deploy mirror reads the
workspace inside that window, sees no lock, and mirrors the transient
NULL as a deletion of the committed `.script.lock` plus a strip of the
`lock: '!inline …'` line — corrupting the git mirror until the relock
writes the identical lock back seconds later.

When pulling (remote -> local), carry the local committed lock onto the
remote map when the remote lock is NULL, so the diff is a no-op for both
the lock file and the metadata line. An empty-string lock ('') — the real
"no dependencies" state — is left untouched, so genuine lock removals
still propagate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): cover __mod multi-module scripts in pending-lock preservation

Address auto-review on #9593: the lock-file key was reconstructed from the
metadata path (`.script.yaml` -> `.script.lock`), so a multi-module script
whose lock lives at `…__mod/script.lock` fell through unprotected. Derive
the key from the committed `!inline` reference instead (covers both the
dotted and `__mod` folder layouts) and detect the folder-layout metadata
file. The reference is always forward-slash; convert to the OS separator so
the local map lookup matches on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:21:38 +02:00

103 lines
3.6 KiB
TypeScript

/**
* Pending-lock preservation (issue #9588)
*
* A git-sync deploy mirror must not delete a committed `.script.lock` when the
* server reports the script's lock as NULL — that NULL is transient (a relock
* is mid-flight), not a real "no dependencies" state. A genuinely lock-free
* script serializes `lock: ''` and must still drop its obsolete lock.
*/
import { expect, test } from "bun:test";
import { stringify as yamlStringify } from "yaml";
import { yamlParseContent } from "../src/utils/yaml.ts";
import { preservePendingScriptLocks } from "../src/commands/sync/sync.ts";
const META = "f/foo/bar.script.yaml";
const LOCK = "f/foo/bar.script.lock";
const LOCK_REF = "!inline f/foo/bar.script.lock";
const LOCK_CONTENT = "//deps\nsome-lock-content\n";
function localMaps() {
return {
[META]: yamlStringify({ summary: "bar", lock: LOCK_REF }),
[LOCK]: LOCK_CONTENT,
};
}
test("NULL remote lock preserves the committed lock file and metadata line", () => {
// Remote during the race: lock key absent (NULL), no lock file emitted.
const remote: Record<string, string> = {
[META]: yamlStringify({ summary: "bar" }),
};
const local = localMaps();
preservePendingScriptLocks(remote, local);
// Lock file is carried onto the remote, so the diff sees no deletion.
expect(remote[LOCK]).toBe(LOCK_CONTENT);
// Metadata regains the inline lock reference, so no metadata edit either.
const parsed = yamlParseContent(META, remote[META]) as Record<string, unknown>;
expect(parsed["lock"]).toBe(LOCK_REF);
});
test("empty-string remote lock still removes the obsolete committed lock", () => {
// Dependencies genuinely removed: lock is '' (present, computed, no deps).
const remote: Record<string, string> = {
[META]: yamlStringify({ summary: "bar", lock: "" }),
};
const local = localMaps();
preservePendingScriptLocks(remote, local);
// Unchanged: no lock file injected, so the diff still deletes the local lock.
expect(remote[LOCK]).toBeUndefined();
const parsed = yamlParseContent(META, remote[META]) as Record<string, unknown>;
expect(parsed["lock"]).toBe("");
});
test("NULL lock without a committed local lock file is a no-op", () => {
const remote: Record<string, string> = {
[META]: yamlStringify({ summary: "bar" }),
};
// Local has the metadata but no committed lock file (nothing to preserve).
const local: Record<string, string> = {
[META]: yamlStringify({ summary: "bar", lock: LOCK_REF }),
};
preservePendingScriptLocks(remote, local);
expect(remote[LOCK]).toBeUndefined();
});
test("NULL remote lock preserves a multi-module (__mod) script lock", () => {
// Multi-module layout: metadata at <dir>/script.yaml, lock at <dir>/script.lock,
// and the inline reference points into the __mod folder.
const meta = "f/foo/bar__mod/script.yaml";
const lock = "f/foo/bar__mod/script.lock";
const lockRef = "!inline f/foo/bar__mod/script.lock";
const remote: Record<string, string> = {
[meta]: yamlStringify({ summary: "bar" }),
};
const local: Record<string, string> = {
[meta]: yamlStringify({ summary: "bar", lock: lockRef }),
[lock]: LOCK_CONTENT,
};
preservePendingScriptLocks(remote, local);
expect(remote[lock]).toBe(LOCK_CONTENT);
const parsed = yamlParseContent(meta, remote[meta]) as Record<string, unknown>;
expect(parsed["lock"]).toBe(lockRef);
});
test("deleted remote script does not resurrect its lock", () => {
// Script removed remotely: no remote metadata key at all.
const remote: Record<string, string> = {};
const local = localMaps();
preservePendingScriptLocks(remote, local);
expect(remote[LOCK]).toBeUndefined();
expect(remote[META]).toBeUndefined();
});