@indigoai-us/hq-cloud 6.0.2 → 6.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/rescue-drift-reconcile.test.js +1 -1
- package/dist/cli/rescue-drift-reconcile.test.js.map +1 -1
- package/dist/cli/rescue-mtime-preserve.test.d.ts +2 -0
- package/dist/cli/rescue-mtime-preserve.test.d.ts.map +1 -0
- package/dist/cli/rescue-mtime-preserve.test.js +162 -0
- package/dist/cli/rescue-mtime-preserve.test.js.map +1 -0
- package/package.json +1 -1
- package/scripts/replace-rescue.sh +90 -11
- package/src/cli/rescue-drift-reconcile.test.ts +1 -1
- package/src/cli/rescue-mtime-preserve.test.ts +182 -0
|
@@ -131,7 +131,7 @@ exec ${JSON.stringify(realGit)} "$@"
|
|
|
131
131
|
// The genuinely-edited file is still rescued into personal/.
|
|
132
132
|
expect(out).toContain("user-edit (rescue): core/edited.md -> personal/edited.md");
|
|
133
133
|
// Untouched file classified UNCHANGED.
|
|
134
|
-
expect(out).toContain("unchanged (
|
|
134
|
+
expect(out).toContain("unchanged (preserved in place): core/keep.md");
|
|
135
135
|
// Summary reflects exactly one reconcile.
|
|
136
136
|
expect(out).toMatch(/drift reconciled \(== upstream\):\s+1 files/);
|
|
137
137
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rescue-drift-reconcile.test.js","sourceRoot":"","sources":["../../src/cli/rescue-drift-reconcile.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,2BAA2B,CAAC,CAAC;AAE/E,SAAS,MAAM;IACb,IAAI,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC;AAE9B,QAAQ,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,gCAAgC,EAAE,GAAG,EAAE;IACpE,IAAI,OAAe,CAAC;IACpB,IAAI,QAAgB,CAAC;IACrB,IAAI,MAAc,CAAC;IACnB,IAAI,OAAe,CAAC;IACpB,IAAI,QAAgB,CAAC;IACrB,IAAI,GAAsB,CAAC;IAE3B,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,GAAG,IAAc,EAAE,EAAE,CAC7C,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;QACxB,GAAG;QACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;QACjC,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,eAAe,EAAE,GAAG;YACpB,gBAAgB,EAAE,KAAK;YACvB,kBAAkB,EAAE,GAAG;YACvB,mBAAmB,EAAE,KAAK;SAC3B;KACF,CAAC;SACC,QAAQ,EAAE;SACV,IAAI,EAAE,CAAC;IAEZ,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;QAErE,sEAAsE;QACtE,wEAAwE;QACxE,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC1C,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QAC/C,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,EAAE,QAAQ,CAAC,CAAC;QAClE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QAChE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3B,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACvC,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAC9C,4BAA4B;QAC5B,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3B,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAEtC,iDAAiD;QACjD,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,4EAA4E;QAC5E,qCAAqC;QACrC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,wEAAwE;QACxE,kEAAkE;QAClE,qDAAqD;QACrD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QACzD,4EAA4E;QAC5E,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAAE,QAAQ,CAAC,CAAC;QAChE,mCAAmC;QACnC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QAE9D,gFAAgF;QAChF,uEAAuE;QACvE,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,cAAc,CAAC;QACpG,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACrC,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAG;;;;;gCAKe,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;;;;SAI/C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;;OAEzB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;CAC7B,CAAC;QACE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAEnE,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,GAAG,EAAE;QACZ,IAAI,OAAO;YAAE,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,SAAS,SAAS;QAChB,OAAO,SAAS,CACd,MAAM,EACN;YACE,aAAa;YACb,WAAW,EAAE,MAAM;YACnB,UAAU,EAAE,WAAW;YACvB,OAAO,EAAE,MAAM;YACf,aAAa,EAAE,QAAQ;YACvB,WAAW;YACX,OAAO;YACP,aAAa;SACd,EACD,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,CAC3B,CAAC;IACJ,CAAC;IAED,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACxF,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QACvC,uDAAuD;QACvD,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,aAAa,QAAQ,EAAE,CAAC,CAAC;QAE/C,qEAAqE;QACrE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,qEAAqE,CAAC,CAAC;QAC7F,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAC;QAE7D,6DAA6D;QAC7D,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,4DAA4D,CAAC,CAAC;QAEpF,uCAAuC;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"rescue-drift-reconcile.test.js","sourceRoot":"","sources":["../../src/cli/rescue-drift-reconcile.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rescue-mtime-preserve.test.d.ts","sourceRoot":"","sources":["../../src/cli/rescue-mtime-preserve.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration regression for the rescue mtime-preservation fix in
|
|
3
|
+
* scripts/replace-rescue.sh.
|
|
4
|
+
*
|
|
5
|
+
* The bug: rescue rebuilds the tree by `git clone` + `rsync -a`. A git
|
|
6
|
+
* checkout stamps every file with clone-time (git stores no per-file mtimes),
|
|
7
|
+
* so the overlay reset EVERY rescued file's mtime to "whenever rescue ran" —
|
|
8
|
+
* collapsing real history onto one instant (the exact fingerprint that flagged
|
|
9
|
+
* this: hundreds of files sharing one mtime, mtime == ctime == birth).
|
|
10
|
+
*
|
|
11
|
+
* The fix has three parts, all asserted below:
|
|
12
|
+
* - Layer 1: UNCHANGED files are left in place; the --checksum overlay skips
|
|
13
|
+
* them, so their existing mtime survives.
|
|
14
|
+
* - Layer 2: files the overlay DOES write (upstream-changed, brand-new) get
|
|
15
|
+
* the git committer-date of their last-modifying commit, not wall-clock.
|
|
16
|
+
* - Layer 3: full history is used (history_floor mode clones full history;
|
|
17
|
+
* the shallow path unshallows) so per-file commit times are real.
|
|
18
|
+
*
|
|
19
|
+
* The script clones from GitHub, so we shim `git clone` to a local fixture
|
|
20
|
+
* (same technique as rescue-drift-reconcile.test.ts) and run for real
|
|
21
|
+
* (non-dry-run, --no-backup) so the overlay actually lays files down.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
24
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
25
|
+
import * as fs from "fs";
|
|
26
|
+
import * as os from "os";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
const RESCUE_SCRIPT = path.resolve(process.cwd(), "scripts/replace-rescue.sh");
|
|
29
|
+
function has(bin, ...args) {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync(bin, args, { stdio: "ignore" });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Needs git (fixture + clone), rsync (overlay), and perl (the utime pass).
|
|
39
|
+
const toolsAvailable = has("git", "--version") && has("rsync", "--version") && has("perl", "-e", "1");
|
|
40
|
+
// Fixed commit epochs so assertions are exact and machine-independent.
|
|
41
|
+
const FLOOR_EPOCH = 1577836800; // 2020-01-01T00:00:00Z
|
|
42
|
+
const HEAD_EPOCH = 1609459200; // 2021-01-01T00:00:00Z
|
|
43
|
+
const KEEP_PRESET_EPOCH = 1546300800; // 2019-01-01T00:00:00Z (local, pre-rescue)
|
|
44
|
+
describe.skipIf(!toolsAvailable)("rescue preserves mtimes (no clone-time flattening)", () => {
|
|
45
|
+
let workDir;
|
|
46
|
+
let upstream;
|
|
47
|
+
let hqRoot;
|
|
48
|
+
let shimDir;
|
|
49
|
+
let floorSha;
|
|
50
|
+
let env;
|
|
51
|
+
const gitAt = (cwd, epoch, ...args) => execFileSync("git", args, {
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
GIT_AUTHOR_NAME: "t",
|
|
57
|
+
GIT_AUTHOR_EMAIL: "t@t",
|
|
58
|
+
GIT_COMMITTER_NAME: "t",
|
|
59
|
+
GIT_COMMITTER_EMAIL: "t@t",
|
|
60
|
+
GIT_AUTHOR_DATE: `${epoch} +0000`,
|
|
61
|
+
GIT_COMMITTER_DATE: `${epoch} +0000`,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
.toString()
|
|
65
|
+
.trim();
|
|
66
|
+
const mtimeSec = (p) => Math.floor(fs.statSync(p).mtimeMs / 1000);
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-rescue-mtime-"));
|
|
69
|
+
// --- "upstream" repo ----------------------------------------------------
|
|
70
|
+
// floor commit (2020): x.md=v1, edited.md=base, keep.md=same
|
|
71
|
+
// HEAD commit (2021): x.md=v2 (changed) + new.md added; keep.md untouched
|
|
72
|
+
upstream = path.join(workDir, "upstream");
|
|
73
|
+
fs.mkdirSync(path.join(upstream, "core"), { recursive: true });
|
|
74
|
+
gitAt(workDir, FLOOR_EPOCH, "init", "-b", "main", "upstream");
|
|
75
|
+
fs.writeFileSync(path.join(upstream, "core/x.md"), "v1\n");
|
|
76
|
+
fs.writeFileSync(path.join(upstream, "core/edited.md"), "base\n");
|
|
77
|
+
fs.writeFileSync(path.join(upstream, "core/keep.md"), "same\n");
|
|
78
|
+
gitAt(upstream, FLOOR_EPOCH, "add", "-A");
|
|
79
|
+
gitAt(upstream, FLOOR_EPOCH, "commit", "-m", "floor");
|
|
80
|
+
floorSha = gitAt(upstream, FLOOR_EPOCH, "rev-parse", "HEAD");
|
|
81
|
+
fs.writeFileSync(path.join(upstream, "core/x.md"), "v2\n");
|
|
82
|
+
fs.writeFileSync(path.join(upstream, "core/new.md"), "fresh\n");
|
|
83
|
+
gitAt(upstream, HEAD_EPOCH, "add", "-A");
|
|
84
|
+
gitAt(upstream, HEAD_EPOCH, "commit", "-m", "head");
|
|
85
|
+
// --- local HQ root being rescued ----------------------------------------
|
|
86
|
+
hqRoot = path.join(workDir, "hq");
|
|
87
|
+
fs.mkdirSync(path.join(hqRoot, "core"), { recursive: true });
|
|
88
|
+
fs.mkdirSync(path.join(hqRoot, "personal"), { recursive: true });
|
|
89
|
+
fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
|
|
90
|
+
// Unchanged vs floor but upstream advanced v1 -> v2: overlay rewrites it.
|
|
91
|
+
fs.writeFileSync(path.join(hqRoot, "core/x.md"), "v1\n");
|
|
92
|
+
// User-edited: rescued to personal/ (not asserted here).
|
|
93
|
+
fs.writeFileSync(path.join(hqRoot, "core/edited.md"), "MINE\n");
|
|
94
|
+
// Identical to upstream HEAD: UNCHANGED -> left in place. Pre-stamp an old
|
|
95
|
+
// mtime that must survive the rescue.
|
|
96
|
+
const keep = path.join(hqRoot, "core/keep.md");
|
|
97
|
+
fs.writeFileSync(keep, "same\n");
|
|
98
|
+
fs.utimesSync(keep, KEEP_PRESET_EPOCH, KEEP_PRESET_EPOCH);
|
|
99
|
+
// --- git shim: redirect `git clone <github-url>` to the local fixture ----
|
|
100
|
+
const realGit = execFileSync("bash", ["-lc", "command -v git"]).toString().trim() || "/usr/bin/git";
|
|
101
|
+
shimDir = path.join(workDir, "shim");
|
|
102
|
+
fs.mkdirSync(shimDir, { recursive: true });
|
|
103
|
+
const shim = `#!/usr/bin/env bash
|
|
104
|
+
if [ "$1" = "clone" ]; then
|
|
105
|
+
args=()
|
|
106
|
+
for a in "$@"; do
|
|
107
|
+
case "$a" in
|
|
108
|
+
https://github.com/*) a=${JSON.stringify(upstream)} ;;
|
|
109
|
+
esac
|
|
110
|
+
args+=("$a")
|
|
111
|
+
done
|
|
112
|
+
exec ${JSON.stringify(realGit)} "\${args[@]}"
|
|
113
|
+
fi
|
|
114
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
115
|
+
`;
|
|
116
|
+
fs.writeFileSync(path.join(shimDir, "git"), shim, { mode: 0o755 });
|
|
117
|
+
env = { ...process.env, PATH: `${shimDir}:${process.env.PATH ?? ""}` };
|
|
118
|
+
// --- run the real rescue (non-dry-run) ----------------------------------
|
|
119
|
+
const r = spawnSync("bash", [
|
|
120
|
+
RESCUE_SCRIPT,
|
|
121
|
+
"--hq-root", hqRoot,
|
|
122
|
+
"--source", "test/repo",
|
|
123
|
+
"--ref", "main",
|
|
124
|
+
"--floor-sha", floorSha,
|
|
125
|
+
"--yes",
|
|
126
|
+
"--no-backup",
|
|
127
|
+
], { env, encoding: "utf-8" });
|
|
128
|
+
// Surface script output on failure for debuggability.
|
|
129
|
+
if (r.status !== 0) {
|
|
130
|
+
throw new Error(`rescue failed (${r.status}):\n${r.stdout}\n${r.stderr}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
afterAll(() => {
|
|
134
|
+
if (workDir)
|
|
135
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
it("rewrites an upstream-changed file with its git commit mtime (not now)", () => {
|
|
138
|
+
const x = path.join(hqRoot, "core/x.md");
|
|
139
|
+
expect(fs.readFileSync(x, "utf-8")).toBe("v2\n");
|
|
140
|
+
expect(mtimeSec(x)).toBe(HEAD_EPOCH);
|
|
141
|
+
});
|
|
142
|
+
it("creates a brand-new upstream file with its git commit mtime", () => {
|
|
143
|
+
const n = path.join(hqRoot, "core/new.md");
|
|
144
|
+
expect(fs.existsSync(n)).toBe(true);
|
|
145
|
+
expect(mtimeSec(n)).toBe(HEAD_EPOCH);
|
|
146
|
+
});
|
|
147
|
+
it("leaves an UNCHANGED file in place, preserving its existing mtime (Layer 1)", () => {
|
|
148
|
+
const keep = path.join(hqRoot, "core/keep.md");
|
|
149
|
+
expect(fs.readFileSync(keep, "utf-8")).toBe("same\n");
|
|
150
|
+
expect(mtimeSec(keep)).toBe(KEEP_PRESET_EPOCH);
|
|
151
|
+
});
|
|
152
|
+
it("never stamps an overlaid file with wall-clock-now (the original bug)", () => {
|
|
153
|
+
const now = Math.floor(Date.now() / 1000);
|
|
154
|
+
for (const rel of ["core/x.md", "core/new.md", "core/keep.md"]) {
|
|
155
|
+
const age = now - mtimeSec(path.join(hqRoot, rel));
|
|
156
|
+
// Every asserted file predates the run by years; a clone-time stamp
|
|
157
|
+
// would be within seconds of now.
|
|
158
|
+
expect(age, `${rel} mtime looks like wall-clock-now`).toBeGreaterThan(60);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
//# sourceMappingURL=rescue-mtime-preserve.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rescue-mtime-preserve.test.js","sourceRoot":"","sources":["../../src/cli/rescue-mtime-preserve.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -467,12 +467,20 @@ fi
|
|
|
467
467
|
TMPDIR="$(mktemp -d -t hq-replace-rescue-XXXXXX)"
|
|
468
468
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
469
469
|
|
|
470
|
-
# Append-only record of every file the walk MOVES (rescue) or
|
|
470
|
+
# Append-only record of every file the walk MOVES (rescue) or LEAVES in place
|
|
471
471
|
# (unchanged). Folded into the snapshot's RECOVERY.md manifest after the run
|
|
472
472
|
# so a user can see exactly what changed and restore any single file.
|
|
473
473
|
RESCUE_LOG="$TMPDIR/rescue-actions.log"
|
|
474
474
|
: > "$RESCUE_LOG"
|
|
475
475
|
|
|
476
|
+
# Paths the walk classified UNCHANGED (left in place). Fed to the overlay as an
|
|
477
|
+
# --exclude-from so rsync never touches them — preserving their on-disk mtime
|
|
478
|
+
# exactly (Layer 1). rsync's -t re-stamps even a checksum-skipped file to the
|
|
479
|
+
# source's time, so excluding is the only way to leave the local mtime truly
|
|
480
|
+
# untouched.
|
|
481
|
+
UNCHANGED_LIST="$TMPDIR/unchanged-paths.txt"
|
|
482
|
+
: > "$UNCHANGED_LIST"
|
|
483
|
+
|
|
476
484
|
# Build the clone URL. If GH_TOKEN is set in the environment, inject it as
|
|
477
485
|
# the basic-auth user so `git clone` can access private staging repos
|
|
478
486
|
# without an interactive credential prompt. This is the form the GitHub
|
|
@@ -518,6 +526,51 @@ fi
|
|
|
518
526
|
SRC_SHA="$(cd "$TMPDIR/src" && git rev-parse HEAD)"
|
|
519
527
|
echo "==> Source SHA: $SRC_SHA"
|
|
520
528
|
|
|
529
|
+
# --- Restore file mtimes from git history (mtime-preservation fix) -----------
|
|
530
|
+
#
|
|
531
|
+
# A bare `git clone`/checkout stamps every working-tree file with clone-time
|
|
532
|
+
# (git stores no per-file mtimes), so the `rsync -a` overlay below would reset
|
|
533
|
+
# every rescued file's mtime to "whenever rescue ran" — collapsing all history
|
|
534
|
+
# onto one instant and breaking "newer-than" comparisons, mtime-keyed caches,
|
|
535
|
+
# and reproducible state across machines. This is the rescue-path analogue of
|
|
536
|
+
# the 5.37.0 `hq-mtime` fix on the sync/S3 path, sourced from git commit times
|
|
537
|
+
# because the rescue source is a clone, not the vault.
|
|
538
|
+
#
|
|
539
|
+
# Single history walk, newest-first: the first commit a path appears in is its
|
|
540
|
+
# last-modifying commit, and that commit's committer-date becomes the file's
|
|
541
|
+
# mtime. Applied via perl's utime() — portable across macOS/Linux, unlike
|
|
542
|
+
# `touch -d @epoch` (GNU-only). Symlinks are skipped (utime follows the link
|
|
543
|
+
# to its target). Deleted-then-readded paths resolve correctly: newest wins.
|
|
544
|
+
restore_mtimes_from_git() {
|
|
545
|
+
local src="$1"
|
|
546
|
+
if ! command -v perl >/dev/null 2>&1; then
|
|
547
|
+
echo " (perl unavailable; skipping mtime restore — overlay mtimes stay clone-time)"
|
|
548
|
+
return 0
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# Correct per-file granularity needs full history. A shallow clone knows
|
|
552
|
+
# only the tip commit, so every file would collapse to the release time;
|
|
553
|
+
# deepen (blob:none keeps it to cheap commit/tree metadata) when shallow.
|
|
554
|
+
if [ "$(git -C "$src" rev-parse --is-shallow-repository 2>/dev/null)" = "true" ]; then
|
|
555
|
+
echo "==> Deepening clone for per-file mtime history (blob:none) ..."
|
|
556
|
+
git -C "$src" fetch --unshallow --filter=blob:none >/dev/null 2>&1 \
|
|
557
|
+
|| echo " (unshallow failed; mtimes fall back to release tip-commit time)"
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
echo "==> Restoring file mtimes from git commit history ..."
|
|
561
|
+
git -C "$src" log --no-renames --pretty=format:'C:%ct' --name-only --diff-filter=ACMR 2>/dev/null \
|
|
562
|
+
| awk '/^C:/ { ts = substr($0, 3); next } NF { if (!seen[$0]++) print ts "\t" $0 }' \
|
|
563
|
+
| SRC="$src" perl -ne '
|
|
564
|
+
chomp;
|
|
565
|
+
my ($ts, $rel) = split(/\t/, $_, 2);
|
|
566
|
+
next unless defined $rel and length $rel;
|
|
567
|
+
my $f = "$ENV{SRC}/$rel";
|
|
568
|
+
next if -l $f or not -f $f;
|
|
569
|
+
utime $ts, $ts, $f;
|
|
570
|
+
' || true
|
|
571
|
+
}
|
|
572
|
+
restore_mtimes_from_git "$TMPDIR/src"
|
|
573
|
+
|
|
521
574
|
# --- Resolve the history floor (last-sync SHA reachable in clone?) ----------
|
|
522
575
|
#
|
|
523
576
|
# The v0.1.104 algorithm drops the path → all-time-SHA index in favour of
|
|
@@ -1090,13 +1143,30 @@ process_one() {
|
|
|
1090
1143
|
fi
|
|
1091
1144
|
fi
|
|
1092
1145
|
else
|
|
1093
|
-
#
|
|
1146
|
+
# user_edited=0: local matches the floor baseline (user didn't touch it).
|
|
1147
|
+
# That does NOT mean it matches upstream HEAD — upstream may have advanced
|
|
1148
|
+
# or removed it. Split on byte-equality to the HEAD version the overlay
|
|
1149
|
+
# would write ($TMPDIR/src is checked out at HEAD):
|
|
1150
|
+
# identical to HEAD -> overlay is a content no-op; leave it untouched and
|
|
1151
|
+
# protect it from the overlay so its mtime is preserved (Layer 1).
|
|
1152
|
+
# differs from HEAD -> keep the original delete semantics: rm now; the
|
|
1153
|
+
# no-delete overlay re-lays it from source (with a git-commit mtime)
|
|
1154
|
+
# when still upstream, or leaves it gone when upstream removed it.
|
|
1094
1155
|
COUNT_UNCHANGED=$((COUNT_UNCHANGED + 1))
|
|
1095
|
-
if
|
|
1096
|
-
|
|
1156
|
+
if cmp -s "$local_path" "$TMPDIR/src/$rel" 2>/dev/null; then
|
|
1157
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
1158
|
+
echo " unchanged (preserved in place): $rel"
|
|
1159
|
+
else
|
|
1160
|
+
printf '/%s\n' "$rel" >> "$UNCHANGED_LIST"
|
|
1161
|
+
printf 'unchanged\t%s\t(identical to upstream; left in place, mtime preserved)\n' "$rel" >> "$RESCUE_LOG" 2>/dev/null || true
|
|
1162
|
+
fi
|
|
1097
1163
|
else
|
|
1098
|
-
|
|
1099
|
-
|
|
1164
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
1165
|
+
echo " unchanged (delete + replace): $rel"
|
|
1166
|
+
else
|
|
1167
|
+
rm -f "$local_path"
|
|
1168
|
+
printf 'deleted\t%s\t(unchanged vs baseline; re-laid by overlay if still upstream)\n' "$rel" >> "$RESCUE_LOG" 2>/dev/null || true
|
|
1169
|
+
fi
|
|
1100
1170
|
fi
|
|
1101
1171
|
fi
|
|
1102
1172
|
}
|
|
@@ -1211,7 +1281,7 @@ if [ "$DRY_RUN" = "1" ]; then
|
|
|
1211
1281
|
echo ""
|
|
1212
1282
|
echo "==> DRY RUN classification summary:"
|
|
1213
1283
|
echo " user-only (leave in place): $COUNT_USER_ONLY files"
|
|
1214
|
-
echo " unchanged (
|
|
1284
|
+
echo " unchanged (preserved/re-laid): $COUNT_UNCHANGED files"
|
|
1215
1285
|
echo " user-edit (rescue / diff-append): $COUNT_USER_EDIT files"
|
|
1216
1286
|
echo " user-edit (conflict quarantine): $COUNT_USER_CONFLICT files"
|
|
1217
1287
|
echo " user-edit (overwrite-safe): $COUNT_USER_OVERWRITE files"
|
|
@@ -1270,7 +1340,7 @@ for sp in "${PRESERVE_SUBPATHS[@]+"${PRESERVE_SUBPATHS[@]}"}"; do
|
|
|
1270
1340
|
done
|
|
1271
1341
|
|
|
1272
1342
|
echo ""
|
|
1273
|
-
echo "==> Walk complete (user-only: $COUNT_USER_ONLY, unchanged
|
|
1343
|
+
echo "==> Walk complete (user-only: $COUNT_USER_ONLY, unchanged: $COUNT_UNCHANGED, user-edit-rescued: $COUNT_USER_EDIT, conflict-quarantined: $COUNT_USER_CONFLICT, overwrite-safe: $COUNT_USER_OVERWRITE, cloud-symlink-reconciled: $COUNT_CLOUD_SYMLINK_RECONCILED, symlinks-dropped: $COUNT_SYMLINK_DROPPED)"
|
|
1274
1344
|
# Note: v0.1.103-and-earlier did a wholesale `rm -rf` of every wipe-set
|
|
1275
1345
|
# top-level entry here. The new walk_and_process does per-file deletion
|
|
1276
1346
|
# (only files that exist in upstream + are unchanged are deleted; user-only
|
|
@@ -1279,7 +1349,16 @@ echo "==> Walk complete (user-only: $COUNT_USER_ONLY, unchanged-deleted: $COUNT_
|
|
|
1279
1349
|
# the walk left alone.
|
|
1280
1350
|
|
|
1281
1351
|
echo "==> Overlaying source onto HQ root ..."
|
|
1282
|
-
|
|
1352
|
+
# Protect UNCHANGED (identical-to-HEAD) files: feed their paths as an
|
|
1353
|
+
# --exclude-from (FIRST in the filter chain, so it wins) so rsync never touches
|
|
1354
|
+
# them — -a's -t would otherwise re-stamp their mtime to the source's time, and
|
|
1355
|
+
# excluding is the only way to leave the local mtime truly untouched (Layer 1).
|
|
1356
|
+
# Every other file under the wipe set was already rm'd by the walk, so the
|
|
1357
|
+
# overlay writes into an absent slot and -a carries the git-commit mtimes
|
|
1358
|
+
# restore_mtimes_from_git stamped onto the source.
|
|
1359
|
+
OVERLAY_PROTECT=()
|
|
1360
|
+
[ -s "$UNCHANGED_LIST" ] && OVERLAY_PROTECT=( --exclude-from="$UNCHANGED_LIST" )
|
|
1361
|
+
rsync -a "${OVERLAY_PROTECT[@]+"${OVERLAY_PROTECT[@]}"}" "${RSYNC_EXCLUDES[@]}" "$TMPDIR/src/" "$HQ_ROOT/"
|
|
1283
1362
|
|
|
1284
1363
|
if [ -s "$TMPDIR/preserve.map" ]; then
|
|
1285
1364
|
echo "==> Restoring preserved sub-paths ..."
|
|
@@ -1355,7 +1434,7 @@ done
|
|
|
1355
1434
|
echo ""
|
|
1356
1435
|
echo "==> Classification:"
|
|
1357
1436
|
echo " user-only (left in place): $COUNT_USER_ONLY files"
|
|
1358
|
-
echo " unchanged (
|
|
1437
|
+
echo " unchanged (preserved/re-laid): $COUNT_UNCHANGED files"
|
|
1359
1438
|
echo " user-edits (rescued): $COUNT_USER_EDIT files"
|
|
1360
1439
|
echo " user-edits (conflict quarantine): $COUNT_USER_CONFLICT files"
|
|
1361
1440
|
echo " user-edits (overwrite-safe): $COUNT_USER_OVERWRITE files"
|
|
@@ -1390,7 +1469,7 @@ if [ "$DO_BACKUP" = "1" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
|
|
|
1390
1469
|
echo ""
|
|
1391
1470
|
echo "## What the rescue did"
|
|
1392
1471
|
echo " user-only (left in place): $COUNT_USER_ONLY"
|
|
1393
|
-
echo " unchanged (
|
|
1472
|
+
echo " unchanged (preserved/re-laid): $COUNT_UNCHANGED"
|
|
1394
1473
|
echo " user-edit (rescued into personal/): $COUNT_USER_EDIT"
|
|
1395
1474
|
echo " user-edit (conflict quarantine): $COUNT_USER_CONFLICT"
|
|
1396
1475
|
echo " user-edit (overwrite-safe): $COUNT_USER_OVERWRITE"
|
|
@@ -150,7 +150,7 @@ exec ${JSON.stringify(realGit)} "$@"
|
|
|
150
150
|
expect(out).toContain("user-edit (rescue): core/edited.md -> personal/edited.md");
|
|
151
151
|
|
|
152
152
|
// Untouched file classified UNCHANGED.
|
|
153
|
-
expect(out).toContain("unchanged (
|
|
153
|
+
expect(out).toContain("unchanged (preserved in place): core/keep.md");
|
|
154
154
|
|
|
155
155
|
// Summary reflects exactly one reconcile.
|
|
156
156
|
expect(out).toMatch(/drift reconciled \(== upstream\):\s+1 files/);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration regression for the rescue mtime-preservation fix in
|
|
3
|
+
* scripts/replace-rescue.sh.
|
|
4
|
+
*
|
|
5
|
+
* The bug: rescue rebuilds the tree by `git clone` + `rsync -a`. A git
|
|
6
|
+
* checkout stamps every file with clone-time (git stores no per-file mtimes),
|
|
7
|
+
* so the overlay reset EVERY rescued file's mtime to "whenever rescue ran" —
|
|
8
|
+
* collapsing real history onto one instant (the exact fingerprint that flagged
|
|
9
|
+
* this: hundreds of files sharing one mtime, mtime == ctime == birth).
|
|
10
|
+
*
|
|
11
|
+
* The fix has three parts, all asserted below:
|
|
12
|
+
* - Layer 1: UNCHANGED files are left in place; the --checksum overlay skips
|
|
13
|
+
* them, so their existing mtime survives.
|
|
14
|
+
* - Layer 2: files the overlay DOES write (upstream-changed, brand-new) get
|
|
15
|
+
* the git committer-date of their last-modifying commit, not wall-clock.
|
|
16
|
+
* - Layer 3: full history is used (history_floor mode clones full history;
|
|
17
|
+
* the shallow path unshallows) so per-file commit times are real.
|
|
18
|
+
*
|
|
19
|
+
* The script clones from GitHub, so we shim `git clone` to a local fixture
|
|
20
|
+
* (same technique as rescue-drift-reconcile.test.ts) and run for real
|
|
21
|
+
* (non-dry-run, --no-backup) so the overlay actually lays files down.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
24
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
25
|
+
import * as fs from "fs";
|
|
26
|
+
import * as os from "os";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
|
|
29
|
+
const RESCUE_SCRIPT = path.resolve(process.cwd(), "scripts/replace-rescue.sh");
|
|
30
|
+
|
|
31
|
+
function has(bin: string, ...args: string[]): boolean {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync(bin, args, { stdio: "ignore" });
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Needs git (fixture + clone), rsync (overlay), and perl (the utime pass).
|
|
41
|
+
const toolsAvailable =
|
|
42
|
+
has("git", "--version") && has("rsync", "--version") && has("perl", "-e", "1");
|
|
43
|
+
|
|
44
|
+
// Fixed commit epochs so assertions are exact and machine-independent.
|
|
45
|
+
const FLOOR_EPOCH = 1577836800; // 2020-01-01T00:00:00Z
|
|
46
|
+
const HEAD_EPOCH = 1609459200; // 2021-01-01T00:00:00Z
|
|
47
|
+
const KEEP_PRESET_EPOCH = 1546300800; // 2019-01-01T00:00:00Z (local, pre-rescue)
|
|
48
|
+
|
|
49
|
+
describe.skipIf(!toolsAvailable)("rescue preserves mtimes (no clone-time flattening)", () => {
|
|
50
|
+
let workDir: string;
|
|
51
|
+
let upstream: string;
|
|
52
|
+
let hqRoot: string;
|
|
53
|
+
let shimDir: string;
|
|
54
|
+
let floorSha: string;
|
|
55
|
+
let env: NodeJS.ProcessEnv;
|
|
56
|
+
|
|
57
|
+
const gitAt = (cwd: string, epoch: number, ...args: string[]) =>
|
|
58
|
+
execFileSync("git", args, {
|
|
59
|
+
cwd,
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
GIT_AUTHOR_NAME: "t",
|
|
64
|
+
GIT_AUTHOR_EMAIL: "t@t",
|
|
65
|
+
GIT_COMMITTER_NAME: "t",
|
|
66
|
+
GIT_COMMITTER_EMAIL: "t@t",
|
|
67
|
+
GIT_AUTHOR_DATE: `${epoch} +0000`,
|
|
68
|
+
GIT_COMMITTER_DATE: `${epoch} +0000`,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
.toString()
|
|
72
|
+
.trim();
|
|
73
|
+
|
|
74
|
+
const mtimeSec = (p: string) => Math.floor(fs.statSync(p).mtimeMs / 1000);
|
|
75
|
+
|
|
76
|
+
beforeAll(() => {
|
|
77
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-rescue-mtime-"));
|
|
78
|
+
|
|
79
|
+
// --- "upstream" repo ----------------------------------------------------
|
|
80
|
+
// floor commit (2020): x.md=v1, edited.md=base, keep.md=same
|
|
81
|
+
// HEAD commit (2021): x.md=v2 (changed) + new.md added; keep.md untouched
|
|
82
|
+
upstream = path.join(workDir, "upstream");
|
|
83
|
+
fs.mkdirSync(path.join(upstream, "core"), { recursive: true });
|
|
84
|
+
gitAt(workDir, FLOOR_EPOCH, "init", "-b", "main", "upstream");
|
|
85
|
+
fs.writeFileSync(path.join(upstream, "core/x.md"), "v1\n");
|
|
86
|
+
fs.writeFileSync(path.join(upstream, "core/edited.md"), "base\n");
|
|
87
|
+
fs.writeFileSync(path.join(upstream, "core/keep.md"), "same\n");
|
|
88
|
+
gitAt(upstream, FLOOR_EPOCH, "add", "-A");
|
|
89
|
+
gitAt(upstream, FLOOR_EPOCH, "commit", "-m", "floor");
|
|
90
|
+
floorSha = gitAt(upstream, FLOOR_EPOCH, "rev-parse", "HEAD");
|
|
91
|
+
fs.writeFileSync(path.join(upstream, "core/x.md"), "v2\n");
|
|
92
|
+
fs.writeFileSync(path.join(upstream, "core/new.md"), "fresh\n");
|
|
93
|
+
gitAt(upstream, HEAD_EPOCH, "add", "-A");
|
|
94
|
+
gitAt(upstream, HEAD_EPOCH, "commit", "-m", "head");
|
|
95
|
+
|
|
96
|
+
// --- local HQ root being rescued ----------------------------------------
|
|
97
|
+
hqRoot = path.join(workDir, "hq");
|
|
98
|
+
fs.mkdirSync(path.join(hqRoot, "core"), { recursive: true });
|
|
99
|
+
fs.mkdirSync(path.join(hqRoot, "personal"), { recursive: true });
|
|
100
|
+
fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
|
|
101
|
+
// Unchanged vs floor but upstream advanced v1 -> v2: overlay rewrites it.
|
|
102
|
+
fs.writeFileSync(path.join(hqRoot, "core/x.md"), "v1\n");
|
|
103
|
+
// User-edited: rescued to personal/ (not asserted here).
|
|
104
|
+
fs.writeFileSync(path.join(hqRoot, "core/edited.md"), "MINE\n");
|
|
105
|
+
// Identical to upstream HEAD: UNCHANGED -> left in place. Pre-stamp an old
|
|
106
|
+
// mtime that must survive the rescue.
|
|
107
|
+
const keep = path.join(hqRoot, "core/keep.md");
|
|
108
|
+
fs.writeFileSync(keep, "same\n");
|
|
109
|
+
fs.utimesSync(keep, KEEP_PRESET_EPOCH, KEEP_PRESET_EPOCH);
|
|
110
|
+
|
|
111
|
+
// --- git shim: redirect `git clone <github-url>` to the local fixture ----
|
|
112
|
+
const realGit = execFileSync("bash", ["-lc", "command -v git"]).toString().trim() || "/usr/bin/git";
|
|
113
|
+
shimDir = path.join(workDir, "shim");
|
|
114
|
+
fs.mkdirSync(shimDir, { recursive: true });
|
|
115
|
+
const shim = `#!/usr/bin/env bash
|
|
116
|
+
if [ "$1" = "clone" ]; then
|
|
117
|
+
args=()
|
|
118
|
+
for a in "$@"; do
|
|
119
|
+
case "$a" in
|
|
120
|
+
https://github.com/*) a=${JSON.stringify(upstream)} ;;
|
|
121
|
+
esac
|
|
122
|
+
args+=("$a")
|
|
123
|
+
done
|
|
124
|
+
exec ${JSON.stringify(realGit)} "\${args[@]}"
|
|
125
|
+
fi
|
|
126
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
127
|
+
`;
|
|
128
|
+
fs.writeFileSync(path.join(shimDir, "git"), shim, { mode: 0o755 });
|
|
129
|
+
env = { ...process.env, PATH: `${shimDir}:${process.env.PATH ?? ""}` };
|
|
130
|
+
|
|
131
|
+
// --- run the real rescue (non-dry-run) ----------------------------------
|
|
132
|
+
const r = spawnSync(
|
|
133
|
+
"bash",
|
|
134
|
+
[
|
|
135
|
+
RESCUE_SCRIPT,
|
|
136
|
+
"--hq-root", hqRoot,
|
|
137
|
+
"--source", "test/repo",
|
|
138
|
+
"--ref", "main",
|
|
139
|
+
"--floor-sha", floorSha,
|
|
140
|
+
"--yes",
|
|
141
|
+
"--no-backup",
|
|
142
|
+
],
|
|
143
|
+
{ env, encoding: "utf-8" },
|
|
144
|
+
);
|
|
145
|
+
// Surface script output on failure for debuggability.
|
|
146
|
+
if (r.status !== 0) {
|
|
147
|
+
throw new Error(`rescue failed (${r.status}):\n${r.stdout}\n${r.stderr}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterAll(() => {
|
|
152
|
+
if (workDir) fs.rmSync(workDir, { recursive: true, force: true });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("rewrites an upstream-changed file with its git commit mtime (not now)", () => {
|
|
156
|
+
const x = path.join(hqRoot, "core/x.md");
|
|
157
|
+
expect(fs.readFileSync(x, "utf-8")).toBe("v2\n");
|
|
158
|
+
expect(mtimeSec(x)).toBe(HEAD_EPOCH);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("creates a brand-new upstream file with its git commit mtime", () => {
|
|
162
|
+
const n = path.join(hqRoot, "core/new.md");
|
|
163
|
+
expect(fs.existsSync(n)).toBe(true);
|
|
164
|
+
expect(mtimeSec(n)).toBe(HEAD_EPOCH);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("leaves an UNCHANGED file in place, preserving its existing mtime (Layer 1)", () => {
|
|
168
|
+
const keep = path.join(hqRoot, "core/keep.md");
|
|
169
|
+
expect(fs.readFileSync(keep, "utf-8")).toBe("same\n");
|
|
170
|
+
expect(mtimeSec(keep)).toBe(KEEP_PRESET_EPOCH);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("never stamps an overlaid file with wall-clock-now (the original bug)", () => {
|
|
174
|
+
const now = Math.floor(Date.now() / 1000);
|
|
175
|
+
for (const rel of ["core/x.md", "core/new.md", "core/keep.md"]) {
|
|
176
|
+
const age = now - mtimeSec(path.join(hqRoot, rel));
|
|
177
|
+
// Every asserted file predates the run by years; a clone-time stamp
|
|
178
|
+
// would be within seconds of now.
|
|
179
|
+
expect(age, `${rel} mtime looks like wall-clock-now`).toBeGreaterThan(60);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|