fix(gsd): snapshot new untracked files before dispatch
This commit is contained in:
parent
7135573324
commit
9a8ae40b25
4 changed files with 49 additions and 9 deletions
|
|
@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
|
|||
import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
|
||||
import { abortAndReset } from "./git-self-heal.js";
|
||||
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
||||
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
||||
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
|
||||
import { getAllWorktreeHealth } from "./worktree-health.js";
|
||||
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
||||
|
||||
|
|
@ -386,19 +386,19 @@ export async function checkGitHealth(
|
|||
code: "stale_uncommitted_changes",
|
||||
scope: "project",
|
||||
unitId: "project",
|
||||
message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
|
||||
message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
|
||||
fixable: true,
|
||||
});
|
||||
|
||||
if (shouldFix("stale_uncommitted_changes")) {
|
||||
try {
|
||||
nativeAddTracked(basePath);
|
||||
nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
|
||||
const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
|
||||
const result = nativeCommit(basePath, commitMsg);
|
||||
if (result) {
|
||||
fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
|
||||
} else {
|
||||
fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
|
||||
fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
|
||||
}
|
||||
} catch {
|
||||
fixesApplied.push("failed to create gsd snapshot commit");
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
|
|||
import { abortAndReset } from "./git-self-heal.js";
|
||||
import { rebuildState } from "./doctor.js";
|
||||
import { deriveState } from "./state.js";
|
||||
import { resolveMilestoneIntegrationBranch } from "./git-service.js";
|
||||
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
||||
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
|
||||
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
|
||||
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
||||
import { runEnvironmentChecks } from "./doctor-environment.js";
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
|
|||
if (minutesSinceCommit >= thresholdMinutes) {
|
||||
const mins = Math.floor(minutesSinceCommit);
|
||||
try {
|
||||
nativeAddTracked(basePath);
|
||||
nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
|
||||
const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
|
||||
const result = nativeCommit(basePath, commitMsg);
|
||||
if (result) {
|
||||
|
|
|
|||
|
|
@ -661,9 +661,10 @@ describe('doctor-git', async () => {
|
|||
env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
|
||||
});
|
||||
|
||||
// Modify an already-tracked file (nativeAddTracked uses git add -u,
|
||||
// which only stages tracked files — new untracked files are not staged)
|
||||
// Modify a tracked file and create a new untracked file. The snapshot
|
||||
// must preserve both, not just tracked changes.
|
||||
writeFileSync(join(dir, "README.md"), "# test\nmodified content\n");
|
||||
writeFileSync(join(dir, "new-untracked.ts"), "export const preserved = true;\n");
|
||||
|
||||
const detect = await runGSDDoctor(dir);
|
||||
const staleIssues = detect.issues.filter(i => i.code === "stale_uncommitted_changes");
|
||||
|
|
@ -681,6 +682,12 @@ describe('doctor-git', async () => {
|
|||
// Verify the snapshot commit was created with the gsd snapshot tag
|
||||
const log = run("git log -1 --oneline", dir);
|
||||
assert.ok(log.includes("gsd snapshot"), "commit is tagged with gsd snapshot");
|
||||
|
||||
const files = run("git show --name-only --format= HEAD", dir);
|
||||
assert.ok(files.includes("README.md"), "snapshot keeps tracked modifications");
|
||||
assert.ok(files.includes("new-untracked.ts"), "snapshot also includes new untracked files");
|
||||
const status = run("git status --short", dir);
|
||||
assert.ok(!status.includes("new-untracked.ts"), "snapshot does not leave the new source file untracked");
|
||||
});
|
||||
|
||||
// ─── Test: stale_uncommitted_changes NOT flagged when recent commit ──
|
||||
|
|
|
|||
|
|
@ -219,6 +219,39 @@ describe('doctor-proactive', async () => {
|
|||
assert.ok(result.fixesApplied.some((f: string) => f.includes("STATE.md")), "reports STATE.md status as info");
|
||||
});
|
||||
|
||||
test('health gate: pre-dispatch snapshot includes new untracked files', async () => {
|
||||
const dir = createRepoWithActiveMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
const pastDate = new Date(Date.now() - 45 * 60 * 1000).toISOString();
|
||||
run(`git commit --amend --no-edit --date="${pastDate}"`, dir);
|
||||
execSync(`git commit --amend --no-edit`, {
|
||||
cwd: dir,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
|
||||
});
|
||||
|
||||
writeFileSync(join(dir, "README.md"), "# test\nmodified content\n");
|
||||
writeFileSync(join(dir, "new-untracked.ts"), "export const preserved = true;\n");
|
||||
|
||||
const result = await preDispatchHealthGate(dir);
|
||||
assert.ok(result.proceed, "dispatch still proceeds after snapshotting");
|
||||
assert.ok(
|
||||
result.fixesApplied.some((f: string) => f.includes("gsd snapshot")),
|
||||
"pre-dispatch gate creates a snapshot commit",
|
||||
);
|
||||
|
||||
const log = run("git log -1 --oneline", dir);
|
||||
assert.ok(log.includes("gsd snapshot"), "snapshot commit is created");
|
||||
|
||||
const files = run("git show --name-only --format= HEAD", dir);
|
||||
assert.ok(files.includes("README.md"), "snapshot keeps tracked modifications");
|
||||
assert.ok(files.includes("new-untracked.ts"), "snapshot also includes new untracked files");
|
||||
const status = run("git status --short", dir);
|
||||
assert.ok(!status.includes("new-untracked.ts"), "snapshot does not leave the new source file untracked");
|
||||
});
|
||||
|
||||
test('health gate: stale crash lock auto-cleared', async () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
|
||||
cleanups.push(dir);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue