gsd-pi 2.82.0-dev.9d5798940 → 2.82.0-dev.dfbc5f58f
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/README.md +2 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +7 -0
- package/dist/resources/extensions/gsd/auto/infra-errors.js +9 -3
- package/dist/resources/extensions/gsd/auto/loop.js +5 -5
- package/dist/resources/extensions/gsd/auto/orchestrator.js +11 -0
- package/dist/resources/extensions/gsd/auto/phases.js +8 -1
- package/dist/resources/extensions/gsd/auto/workflow-memory-pressure.js +12 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +2 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-start.js +78 -9
- package/dist/resources/extensions/gsd/auto-worktree.js +15 -1
- package/dist/resources/extensions/gsd/auto.js +30 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +9 -8
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +5 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +31 -5
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +1 -0
- package/dist/resources/extensions/gsd/dispatch-guard.js +2 -2
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +28 -11
- package/dist/resources/extensions/gsd/doctor.js +2 -28
- package/dist/resources/extensions/gsd/git-service.js +39 -1
- package/dist/resources/extensions/gsd/gsd-db.js +1 -0
- package/dist/resources/extensions/gsd/guided-flow.js +6 -0
- package/dist/resources/extensions/gsd/migrate/parsers.js +10 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +40 -9
- package/dist/resources/extensions/gsd/post-execution-checks.js +73 -2
- package/dist/resources/extensions/gsd/pre-execution-checks.js +28 -1
- package/dist/resources/extensions/gsd/prompt-loader.js +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +4 -0
- package/dist/resources/extensions/gsd/templates/plan.md +8 -5
- package/dist/resources/extensions/gsd/templates/task-plan.md +4 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -8
- package/dist/resources/extensions/gsd/tools/complete-slice.js +6 -8
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +7 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +88 -14
- package/dist/resources/extensions/gsd/validation.js +23 -1
- package/dist/resources/extensions/gsd/verification-gate.js +68 -7
- package/dist/resources/extensions/gsd/workflow-projections.js +6 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +5 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +1 -1
- package/packages/native/tsconfig.json +2 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +82 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +52 -0
- package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts +2 -4
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +5 -6
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/simple-options.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/simple-options.test.js +50 -0
- package/packages/pi-ai/dist/providers/simple-options.test.js.map +1 -0
- package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +63 -0
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +91 -1
- package/packages/pi-ai/src/providers/simple-options.test.ts +60 -0
- package/packages/pi-ai/src/providers/simple-options.ts +5 -6
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js +66 -0
- package/packages/pi-coding-agent/dist/core/agent-session-thinking-level.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-thinking-level.test.ts +79 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/GSD-WORKFLOW.md +7 -0
- package/src/resources/extensions/gsd/auto/contracts.ts +14 -6
- package/src/resources/extensions/gsd/auto/infra-errors.ts +9 -3
- package/src/resources/extensions/gsd/auto/loop.ts +8 -5
- package/src/resources/extensions/gsd/auto/orchestrator.ts +11 -0
- package/src/resources/extensions/gsd/auto/phases.ts +7 -1
- package/src/resources/extensions/gsd/auto/workflow-memory-pressure.ts +13 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +2 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +85 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -1
- package/src/resources/extensions/gsd/auto.ts +32 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +9 -8
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +3 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +30 -4
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +1 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -2
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +25 -13
- package/src/resources/extensions/gsd/doctor.ts +2 -27
- package/src/resources/extensions/gsd/git-service.ts +45 -1
- package/src/resources/extensions/gsd/gsd-db.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +6 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +11 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +46 -9
- package/src/resources/extensions/gsd/post-execution-checks.ts +87 -2
- package/src/resources/extensions/gsd/pre-execution-checks.ts +32 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +5 -0
- package/src/resources/extensions/gsd/templates/plan.md +8 -5
- package/src/resources/extensions/gsd/templates/task-plan.md +4 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +80 -1
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -2
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/guided-flow.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/hook-model-resolution.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +103 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +200 -1
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +86 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/prompt-loader.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +26 -2
- package/src/resources/extensions/gsd/tests/summary-render-parity.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +110 -1
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/workflow-memory-pressure.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +7 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +8 -10
- package/src/resources/extensions/gsd/tools/complete-slice.ts +6 -8
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +5 -1
- package/src/resources/extensions/gsd/tools/plan-slice.ts +96 -12
- package/src/resources/extensions/gsd/types.ts +1 -1
- package/src/resources/extensions/gsd/validation.ts +23 -1
- package/src/resources/extensions/gsd/verification-gate.ts +78 -6
- package/src/resources/extensions/gsd/workflow-projections.ts +6 -8
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -1
- /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BdZQhe8yKl6bdKLiXVEzh → q0WYuDVbHeFFYbdd-fei2}/_ssgManifest.js +0 -0
|
@@ -13,17 +13,19 @@
|
|
|
13
13
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
import { chmodSync, existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
16
|
+
import { delimiter, join } from "node:path";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
19
|
import {
|
|
20
20
|
assertWorktreeMaterialized,
|
|
21
21
|
nativeBranchDelete,
|
|
22
22
|
nativeCommit,
|
|
23
|
+
nativeGetCurrentBranch,
|
|
23
24
|
nativeIsRepo,
|
|
24
25
|
nativeResetHard,
|
|
25
26
|
nativeWorktreeAdd,
|
|
26
27
|
} from "../native-git-bridge.js";
|
|
28
|
+
import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
|
|
27
29
|
|
|
28
30
|
// Note: prior static-analysis tests that scanned native-git-bridge.ts for
|
|
29
31
|
// the raw shell-spawn pattern were removed under #4827 — the integration
|
|
@@ -78,6 +80,55 @@ describe("native-git-bridge #4180: fallback runtime behaviour", () => {
|
|
|
78
80
|
assert.equal(subject, "test: regression commit #4180");
|
|
79
81
|
});
|
|
80
82
|
|
|
83
|
+
test("nativeCommit retries once after transient ENOBUFS from git", (t) => {
|
|
84
|
+
const bin = mkdtempSync(join(tmpdir(), "ngb-enobufs-bin-"));
|
|
85
|
+
t.after(() => rmSync(bin, { recursive: true, force: true }));
|
|
86
|
+
|
|
87
|
+
const realGit = execFileSync("git", ["--exec-path"], { encoding: "utf-8" }).trim();
|
|
88
|
+
const attempts = join(bin, "attempts.txt");
|
|
89
|
+
const fakeGit = join(bin, "fake-git.cjs");
|
|
90
|
+
writeFileSync(fakeGit, `
|
|
91
|
+
const { appendFileSync, readFileSync } = require("node:fs");
|
|
92
|
+
const { spawnSync } = require("node:child_process");
|
|
93
|
+
const attempts = ${JSON.stringify(attempts)};
|
|
94
|
+
const realGit = ${JSON.stringify(join(realGit, process.platform === "win32" ? "git.exe" : "git"))};
|
|
95
|
+
appendFileSync(attempts, "1");
|
|
96
|
+
if (process.argv[2] === "commit" && readFileSync(attempts, "utf-8").length === 1) {
|
|
97
|
+
console.error("spawnSync git ENOBUFS");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const result = spawnSync(realGit, process.argv.slice(2), { stdio: "inherit" });
|
|
101
|
+
process.exit(result.status ?? 1);
|
|
102
|
+
`, "utf-8");
|
|
103
|
+
|
|
104
|
+
if (process.platform === "win32") {
|
|
105
|
+
writeFileSync(join(bin, "git.cmd"), `@echo off\r\nnode "${fakeGit}" %*\r\n`, "utf-8");
|
|
106
|
+
} else {
|
|
107
|
+
const shim = join(bin, "git");
|
|
108
|
+
writeFileSync(shim, `#!/bin/sh\nexec node "${fakeGit}" "$@"\n`, "utf-8");
|
|
109
|
+
chmodSync(shim, 0o755);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
writeFileSync(join(repo, "file.txt"), "retry commit\n");
|
|
113
|
+
git(["add", "."], repo);
|
|
114
|
+
|
|
115
|
+
const originalPath = process.env.PATH ?? "";
|
|
116
|
+
const gitEnv = GIT_NO_PROMPT_ENV as NodeJS.ProcessEnv;
|
|
117
|
+
const originalGitEnvPath = gitEnv.PATH;
|
|
118
|
+
try {
|
|
119
|
+
process.env.PATH = `${bin}${delimiter}${originalPath}`;
|
|
120
|
+
gitEnv.PATH = process.env.PATH;
|
|
121
|
+
const result = nativeCommit(repo, "test: retry ENOBUFS commit");
|
|
122
|
+
assert.ok(result !== null, "commit should succeed after retry");
|
|
123
|
+
} finally {
|
|
124
|
+
process.env.PATH = originalPath;
|
|
125
|
+
gitEnv.PATH = originalGitEnvPath;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
assert.equal(readFileSync(attempts, "utf-8").length, 2);
|
|
129
|
+
assert.equal(git(["log", "-1", "--format=%s"], repo), "test: retry ENOBUFS commit");
|
|
130
|
+
});
|
|
131
|
+
|
|
81
132
|
test("nativeCommit runs commit hooks", () => {
|
|
82
133
|
const hookPath = join(repo, ".git", "hooks", "commit-msg");
|
|
83
134
|
const marker = join(repo, "hook-ran.txt");
|
|
@@ -119,7 +170,17 @@ describe("native-git-bridge #4180: fallback runtime behaviour", () => {
|
|
|
119
170
|
test("nativeBranchDelete throws when git cannot delete the branch", () => {
|
|
120
171
|
assert.throws(
|
|
121
172
|
() => nativeBranchDelete(repo, "does-not-exist"),
|
|
122
|
-
/
|
|
173
|
+
/git branch -D does-not-exist failed[\s\S]*does-not-exist/,
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("nativeGetCurrentBranch preserves git stderr in fallback errors", (t) => {
|
|
178
|
+
const dir = mkdtempSync(join(tmpdir(), "ngb-stderr-notrepo-"));
|
|
179
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
180
|
+
|
|
181
|
+
assert.throws(
|
|
182
|
+
() => nativeGetCurrentBranch(dir),
|
|
183
|
+
/git branch --show-current failed[\s\S]*(not a git repository|not a git repo|fatal:)/,
|
|
123
184
|
);
|
|
124
185
|
});
|
|
125
186
|
|
|
@@ -243,7 +243,7 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
243
243
|
);
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
-
test("
|
|
246
|
+
test("milestone in DB, no branch, no worktree dir → no-op", () => {
|
|
247
247
|
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
248
248
|
|
|
249
249
|
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
@@ -251,4 +251,124 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
251
251
|
assert.deepStrictEqual(result.recovered, []);
|
|
252
252
|
assert.deepStrictEqual(result.warnings, []);
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
test("#5879 — cleans orphaned worktree dir for complete milestone whose branch was already deleted", () => {
|
|
256
|
+
// Reproduces the postflight-stash-restore-failed scenario:
|
|
257
|
+
// 1. An earlier audit deleted milestone/M001 (merged + complete).
|
|
258
|
+
// 2. The worktree dir cleanup failed silently (logWarning only).
|
|
259
|
+
// 3. On the next startup the branch is gone, so the existing branch-keyed
|
|
260
|
+
// loop is invisible to the orphan dir. Without the second pass, the
|
|
261
|
+
// directory lives forever.
|
|
262
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
263
|
+
|
|
264
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
265
|
+
mkdirSync(wtDir, { recursive: true });
|
|
266
|
+
writeFileSync(join(wtDir, "leftover.txt"), "stranded from a prior session\n");
|
|
267
|
+
|
|
268
|
+
// No milestone/M001 branch — already deleted on a previous run.
|
|
269
|
+
const branches = run("git branch --list milestone/M001", dir);
|
|
270
|
+
assert.equal(branches, "", "test fixture: branch should not exist");
|
|
271
|
+
|
|
272
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
273
|
+
|
|
274
|
+
assert.ok(
|
|
275
|
+
result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
|
|
276
|
+
`should report branch-less orphan cleanup; got: ${JSON.stringify(result.recovered)}`,
|
|
277
|
+
);
|
|
278
|
+
assert.ok(!existsSync(wtDir), "branch-less orphan worktree dir should be removed");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("#5879 — branch list failure still cleans complete orphan only after branch absence is verified", () => {
|
|
282
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
283
|
+
|
|
284
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
285
|
+
mkdirSync(wtDir, { recursive: true });
|
|
286
|
+
writeFileSync(join(wtDir, "leftover.txt"), "stranded from a prior session\n");
|
|
287
|
+
|
|
288
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree", {
|
|
289
|
+
branchList: () => {
|
|
290
|
+
throw new Error("branch list failed");
|
|
291
|
+
},
|
|
292
|
+
branchExists: (_basePath, branch) => {
|
|
293
|
+
assert.equal(branch, "milestone/M001");
|
|
294
|
+
return false;
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
assert.ok(
|
|
299
|
+
result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
|
|
300
|
+
`should report verified branch-less orphan cleanup; got: ${JSON.stringify(result.recovered)}`,
|
|
301
|
+
);
|
|
302
|
+
assert.ok(!existsSync(wtDir), "verified branch-less orphan worktree dir should be removed");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("#5879 — branch list failure preserves complete worktree when branch still exists", () => {
|
|
306
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
307
|
+
|
|
308
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
309
|
+
mkdirSync(wtDir, { recursive: true });
|
|
310
|
+
writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
|
|
311
|
+
|
|
312
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree", {
|
|
313
|
+
branchList: () => {
|
|
314
|
+
throw new Error("branch list failed");
|
|
315
|
+
},
|
|
316
|
+
branchExists: (_basePath, branch) => {
|
|
317
|
+
assert.equal(branch, "milestone/M001");
|
|
318
|
+
return true;
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
323
|
+
assert.ok(existsSync(wtDir), "worktree dir must be preserved when branch existence is verified");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("#5879 — skips branch-less orphan for milestone that is not complete", () => {
|
|
327
|
+
// Defensive: only `complete` milestones get the branch-less cleanup. An
|
|
328
|
+
// `active` milestone with no branch but a worktree dir is a different
|
|
329
|
+
// state (probably mid-recovery) and should not be silently wiped.
|
|
330
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
331
|
+
|
|
332
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
333
|
+
mkdirSync(wtDir, { recursive: true });
|
|
334
|
+
writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
|
|
335
|
+
|
|
336
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
337
|
+
|
|
338
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
339
|
+
assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("#5879 — branch list failure does not delete worktree for milestone that is not complete", () => {
|
|
343
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
344
|
+
|
|
345
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
346
|
+
mkdirSync(wtDir, { recursive: true });
|
|
347
|
+
writeFileSync(join(wtDir, "live-work.txt"), "do not delete\n");
|
|
348
|
+
|
|
349
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree", {
|
|
350
|
+
branchList: () => {
|
|
351
|
+
throw new Error("branch list failed");
|
|
352
|
+
},
|
|
353
|
+
branchExists: () => {
|
|
354
|
+
throw new Error("branchExists should not be called for active milestones");
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
359
|
+
assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("#5879 — skips branch-less orphan in 'none' isolation mode", () => {
|
|
363
|
+
insertMilestone({ id: "M001", title: "Test", status: "complete" });
|
|
364
|
+
|
|
365
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
366
|
+
mkdirSync(wtDir, { recursive: true });
|
|
367
|
+
writeFileSync(join(wtDir, "leftover.txt"), "stranded\n");
|
|
368
|
+
|
|
369
|
+
const result = auditOrphanedMilestoneBranches(dir, "none");
|
|
370
|
+
|
|
371
|
+
assert.deepStrictEqual(result.recovered, []);
|
|
372
|
+
assert.ok(existsSync(wtDir), "'none' mode must not touch worktree dirs");
|
|
373
|
+
});
|
|
254
374
|
});
|
|
@@ -116,6 +116,32 @@ test('handlePlanMilestone rejects invalid payloads', async () => {
|
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
test('handlePlanMilestone rejects delimiter characters in milestone and slice titles', async () => {
|
|
120
|
+
const base = makeTmpBase();
|
|
121
|
+
const dbPath = join(base, '.gsd', 'gsd.db');
|
|
122
|
+
openDatabase(dbPath);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const milestoneResult = await handlePlanMilestone({ ...validParams(), title: 'Client/Server split' }, base);
|
|
126
|
+
assert.ok('error' in milestoneResult);
|
|
127
|
+
assert.match(milestoneResult.error, /validation failed: title is invalid: .*forward slash/);
|
|
128
|
+
assert.equal(getMilestone('M001'), null, 'invalid milestone title must not persist');
|
|
129
|
+
|
|
130
|
+
const sliceResult = await handlePlanMilestone({
|
|
131
|
+
...validParams(),
|
|
132
|
+
slices: [
|
|
133
|
+
validParams().slices[0],
|
|
134
|
+
{ ...validParams().slices[1], title: 'Client/Server migration' },
|
|
135
|
+
],
|
|
136
|
+
}, base);
|
|
137
|
+
assert.ok('error' in sliceResult);
|
|
138
|
+
assert.match(sliceResult.error, /validation failed: slices\[1\]\.title is invalid: .*forward slash/);
|
|
139
|
+
assert.equal(getMilestoneSlices('M001').length, 0, 'invalid slice title must not persist partial roadmap state');
|
|
140
|
+
} finally {
|
|
141
|
+
cleanup(base);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
119
145
|
test('handlePlanMilestone surfaces render failures and does not clear parse-visible state on failure', async () => {
|
|
120
146
|
const base = makeTmpBase();
|
|
121
147
|
const dbPath = join(base, '.gsd', 'gsd.db');
|
|
@@ -6,7 +6,7 @@ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
|
|
9
|
-
import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, getSliceTasks, getTask } from '../gsd-db.ts';
|
|
9
|
+
import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, getSliceTasks, getTask, getGateResults, updateTaskStatus } from '../gsd-db.ts';
|
|
10
10
|
import { handlePlanSlice } from '../tools/plan-slice.ts';
|
|
11
11
|
import { parsePlan } from '../parsers-legacy.ts';
|
|
12
12
|
import { parseTaskPlanFile } from '../files.ts';
|
|
@@ -15,6 +15,10 @@ import { deriveState, invalidateStateCache } from '../state.ts';
|
|
|
15
15
|
function makeTmpBase(): string {
|
|
16
16
|
const base = mkdtempSync(join(tmpdir(), 'gsd-plan-slice-'));
|
|
17
17
|
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks'), { recursive: true });
|
|
18
|
+
mkdirSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools'), { recursive: true });
|
|
19
|
+
writeFileSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-milestone.ts'), '// fixture\n', 'utf-8');
|
|
20
|
+
writeFileSync(join(base, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-task.ts'), '// fixture\n', 'utf-8');
|
|
21
|
+
writeFileSync(join(base, 'stale-input.py'), '# fixture\n', 'utf-8');
|
|
18
22
|
return base;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -197,6 +201,28 @@ test('handlePlanSlice rejects invalid payloads', async () => {
|
|
|
197
201
|
}
|
|
198
202
|
});
|
|
199
203
|
|
|
204
|
+
test('handlePlanSlice explains string task IO fields must be arrays', async () => {
|
|
205
|
+
const base = makeTmpBase();
|
|
206
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
seedParentSlice();
|
|
210
|
+
const result = await handlePlanSlice({
|
|
211
|
+
...validParams(),
|
|
212
|
+
tasks: [
|
|
213
|
+
{
|
|
214
|
+
...validParams().tasks[0],
|
|
215
|
+
inputs: 'src/index.ts' as unknown as string[],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
}, base);
|
|
219
|
+
assert.ok('error' in result);
|
|
220
|
+
assert.match(result.error, /validation failed: tasks\[0\]\.inputs must be an array of strings, not string/);
|
|
221
|
+
} finally {
|
|
222
|
+
cleanup(base);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
200
226
|
test('handlePlanSlice rejects absolute task IO paths outside the active worktree', async () => {
|
|
201
227
|
const base = makeTmpBase();
|
|
202
228
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -223,6 +249,63 @@ test('handlePlanSlice rejects absolute task IO paths outside the active worktree
|
|
|
223
249
|
}
|
|
224
250
|
});
|
|
225
251
|
|
|
252
|
+
test('handlePlanSlice rejects missing task input paths before persisting tasks', async () => {
|
|
253
|
+
const base = makeTmpBase();
|
|
254
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
seedParentSlice();
|
|
258
|
+
const result = await handlePlanSlice({
|
|
259
|
+
...validParams(),
|
|
260
|
+
tasks: [
|
|
261
|
+
{
|
|
262
|
+
...validParams().tasks[0],
|
|
263
|
+
inputs: ['fixtures/missing-source.json'],
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
}, base);
|
|
267
|
+
|
|
268
|
+
assert.ok('error' in result);
|
|
269
|
+
assert.match(result.error, /pre-execution validation failed:/);
|
|
270
|
+
assert.match(result.error, /fixtures\/missing-source\.json/);
|
|
271
|
+
assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid planning IO must not persist tasks');
|
|
272
|
+
} finally {
|
|
273
|
+
cleanup(base);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('handlePlanSlice rejects task input paths created by later tasks before persisting tasks', async () => {
|
|
278
|
+
const base = makeTmpBase();
|
|
279
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
seedParentSlice();
|
|
283
|
+
const params = validParams();
|
|
284
|
+
const result = await handlePlanSlice({
|
|
285
|
+
...params,
|
|
286
|
+
tasks: [
|
|
287
|
+
{
|
|
288
|
+
...params.tasks[0],
|
|
289
|
+
inputs: ['generated/report.json'],
|
|
290
|
+
expectedOutput: ['generated/summary.json'],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
...params.tasks[1],
|
|
294
|
+
inputs: [],
|
|
295
|
+
expectedOutput: ['generated/report.json'],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
}, base);
|
|
299
|
+
|
|
300
|
+
assert.ok('error' in result);
|
|
301
|
+
assert.match(result.error, /pre-execution validation failed:/);
|
|
302
|
+
assert.match(result.error, /sequence violation/);
|
|
303
|
+
assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid task ordering must not persist tasks');
|
|
304
|
+
} finally {
|
|
305
|
+
cleanup(base);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
226
309
|
test('handlePlanSlice accepts absolute task IO paths inside the active worktree', async () => {
|
|
227
310
|
const base = makeTmpBase();
|
|
228
311
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -329,3 +412,119 @@ test('handlePlanSlice reruns idempotently and refreshes parse-visible state', as
|
|
|
329
412
|
cleanup(base);
|
|
330
413
|
}
|
|
331
414
|
});
|
|
415
|
+
|
|
416
|
+
test('handlePlanSlice removes omitted pending tasks when replanning a smaller task set', async () => {
|
|
417
|
+
const base = makeTmpBase();
|
|
418
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
seedParentSlice();
|
|
422
|
+
const fourTaskPlan = {
|
|
423
|
+
...validParams(),
|
|
424
|
+
tasks: [
|
|
425
|
+
...validParams().tasks,
|
|
426
|
+
{ ...validParams().tasks[0], taskId: 'T03', title: 'Third task' },
|
|
427
|
+
{ ...validParams().tasks[0], taskId: 'T04', title: 'Stale task', inputs: ['stale-input.py'] },
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const first = await handlePlanSlice(fourTaskPlan, base);
|
|
432
|
+
assert.ok(!('error' in first), `unexpected error: ${'error' in first ? first.error : ''}`);
|
|
433
|
+
const staleTaskPlanPath = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T04-PLAN.md');
|
|
434
|
+
assert.ok(existsSync(staleTaskPlanPath), 'initial plan should render T04');
|
|
435
|
+
|
|
436
|
+
const second = await handlePlanSlice({
|
|
437
|
+
...validParams(),
|
|
438
|
+
tasks: fourTaskPlan.tasks.filter((task) => task.taskId !== 'T04'),
|
|
439
|
+
}, base);
|
|
440
|
+
assert.ok(!('error' in second), `unexpected error: ${'error' in second ? second.error : ''}`);
|
|
441
|
+
|
|
442
|
+
assert.deepEqual(getSliceTasks('M001', 'S02').map((task) => task.id), ['T01', 'T02', 'T03']);
|
|
443
|
+
assert.equal(getGateResults('M001', 'S02', 'task').some((gate) => gate.task_id === 'T04'), false);
|
|
444
|
+
assert.equal(existsSync(staleTaskPlanPath), false, 'omitted task plan artifact should be removed');
|
|
445
|
+
} finally {
|
|
446
|
+
cleanup(base);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('handlePlanSlice rejects omitted completed tasks without changing slice or task state', async () => {
|
|
451
|
+
const base = makeTmpBase();
|
|
452
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
seedParentSlice();
|
|
456
|
+
const fourTaskPlan = {
|
|
457
|
+
...validParams(),
|
|
458
|
+
tasks: [
|
|
459
|
+
...validParams().tasks,
|
|
460
|
+
{ ...validParams().tasks[0], taskId: 'T03', title: 'Third task' },
|
|
461
|
+
{ ...validParams().tasks[0], taskId: 'T04', title: 'Stale task', inputs: ['stale-input.py'] },
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const first = await handlePlanSlice(fourTaskPlan, base);
|
|
466
|
+
assert.ok(!('error' in first), `unexpected error: ${'error' in first ? first.error : ''}`);
|
|
467
|
+
const staleTaskPlanPath = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T04-PLAN.md');
|
|
468
|
+
assert.ok(existsSync(staleTaskPlanPath), 'initial plan should render T04');
|
|
469
|
+
|
|
470
|
+
updateTaskStatus('M001', 'S02', 'T04', 'complete', '2026-05-12T00:00:00.000Z');
|
|
471
|
+
const tasksBefore = getSliceTasks('M001', 'S02');
|
|
472
|
+
const gatesBefore = getGateResults('M001', 'S02', 'task');
|
|
473
|
+
|
|
474
|
+
const second = await handlePlanSlice({
|
|
475
|
+
...validParams(),
|
|
476
|
+
goal: 'Rejected replan should not persist.',
|
|
477
|
+
tasks: fourTaskPlan.tasks.filter((task) => task.taskId !== 'T04'),
|
|
478
|
+
}, base);
|
|
479
|
+
assert.deepEqual(second, { error: 'cannot remove completed task T04' });
|
|
480
|
+
|
|
481
|
+
assert.equal(getSlice('M001', 'S02')?.goal, 'Persist slice planning through the DB.');
|
|
482
|
+
assert.deepEqual(getSliceTasks('M001', 'S02'), tasksBefore);
|
|
483
|
+
assert.deepEqual(getGateResults('M001', 'S02', 'task'), gatesBefore);
|
|
484
|
+
assert.ok(existsSync(staleTaskPlanPath), 'completed task plan artifact should remain after rejected replan');
|
|
485
|
+
} finally {
|
|
486
|
+
cleanup(base);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('regression: validateTasks surfaces clean per-field errors for non-array IO inputs', async () => {
|
|
491
|
+
// Regression for the bug fixed in PR #5872: an earlier refactor on main
|
|
492
|
+
// (0b0e1a901) re-added validateStringArray() calls inside validateTasks
|
|
493
|
+
// without re-adding its import. The catch around validateParams swallowed
|
|
494
|
+
// the ReferenceError into a generic "validation failed: validateStringArray
|
|
495
|
+
// is not defined" message, so silent runtime breakage was possible.
|
|
496
|
+
//
|
|
497
|
+
// Exercise every validateStringArray call site (files, inputs, expectedOutput)
|
|
498
|
+
// so a future missing-import would surface as a per-field assertion failure
|
|
499
|
+
// here, not a deep ReferenceError that's easy to mis-diagnose.
|
|
500
|
+
const base = makeTmpBase();
|
|
501
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
seedParentSlice();
|
|
505
|
+
|
|
506
|
+
for (const field of ['files', 'inputs', 'expectedOutput'] as const) {
|
|
507
|
+
const result = await handlePlanSlice({
|
|
508
|
+
...validParams(),
|
|
509
|
+
tasks: [{
|
|
510
|
+
...validParams().tasks[0],
|
|
511
|
+
[field]: 'not-an-array' as unknown as string[],
|
|
512
|
+
}],
|
|
513
|
+
}, base);
|
|
514
|
+
assert.ok('error' in result, `${field}: expected validation error, got success`);
|
|
515
|
+
assert.match(
|
|
516
|
+
result.error,
|
|
517
|
+
new RegExp(`tasks\\[0\\]\\.${field} must be an array`),
|
|
518
|
+
`${field}: expected per-field validation message, got: ${result.error}`,
|
|
519
|
+
);
|
|
520
|
+
assert.doesNotMatch(
|
|
521
|
+
result.error,
|
|
522
|
+
/is not defined/,
|
|
523
|
+
`${field}: validation surfaced ReferenceError — likely a missing import in plan-slice.ts`,
|
|
524
|
+
);
|
|
525
|
+
assert.equal(getSliceTasks('M001', 'S02').length, 0, `${field}: invalid input must not persist`);
|
|
526
|
+
}
|
|
527
|
+
} finally {
|
|
528
|
+
cleanup(base);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
@@ -79,6 +79,23 @@ test('handlePlanTask rejects invalid payloads', async () => {
|
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('handlePlanTask explains string IO fields must be arrays', async () => {
|
|
83
|
+
const base = makeTmpBase();
|
|
84
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
seedParent();
|
|
88
|
+
const result = await handlePlanTask({
|
|
89
|
+
...validParams(),
|
|
90
|
+
expectedOutput: 'src/output.ts' as unknown as string[],
|
|
91
|
+
}, base);
|
|
92
|
+
assert.ok('error' in result);
|
|
93
|
+
assert.match(result.error, /validation failed: expectedOutput must be an array of strings, not string/);
|
|
94
|
+
} finally {
|
|
95
|
+
cleanup(base);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
82
99
|
test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
|
|
83
100
|
const base = makeTmpBase();
|
|
84
101
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -127,6 +127,61 @@ import { b } from './b';
|
|
|
127
127
|
assert.equal(imports.length, 2);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test("ignores import-looking string literals in test fixtures", () => {
|
|
131
|
+
const source = `
|
|
132
|
+
const rewritten = source.replace(
|
|
133
|
+
'import { normalizeZagrebBusinessDeadline } from "./cutoff";',
|
|
134
|
+
'const helper = true;'
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
import realThing from "./real-thing";
|
|
138
|
+
`;
|
|
139
|
+
const imports = extractRelativeImports(source);
|
|
140
|
+
assert.deepEqual(imports, [
|
|
141
|
+
{ importPath: "./real-thing", lineNum: 7 },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("ignores import-looking lines inside template literals", () => {
|
|
146
|
+
const source = [
|
|
147
|
+
"const fixture = `",
|
|
148
|
+
"import missingThing from './missing-thing';",
|
|
149
|
+
"`;",
|
|
150
|
+
"",
|
|
151
|
+
"import realThing from './real-thing';",
|
|
152
|
+
].join("\n");
|
|
153
|
+
const imports = extractRelativeImports(source);
|
|
154
|
+
assert.deepEqual(imports, [
|
|
155
|
+
{ importPath: "./real-thing", lineNum: 5 },
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("ignores require() inside string literals", () => {
|
|
160
|
+
const source = [
|
|
161
|
+
'const fixture = "const x = require(\'./missing\');";',
|
|
162
|
+
"const otherFixture = 'const y = require(\"./also-missing\");';",
|
|
163
|
+
"const real = require('./real');",
|
|
164
|
+
].join("\n");
|
|
165
|
+
const imports = extractRelativeImports(source);
|
|
166
|
+
assert.deepEqual(imports, [
|
|
167
|
+
{ importPath: "./real", lineNum: 3 },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("ignores require() inside template literals", () => {
|
|
172
|
+
const source = [
|
|
173
|
+
"const fixture = `",
|
|
174
|
+
"const x = require('./missing');",
|
|
175
|
+
"`;",
|
|
176
|
+
"",
|
|
177
|
+
"const real = require('./real');",
|
|
178
|
+
].join("\n");
|
|
179
|
+
const imports = extractRelativeImports(source);
|
|
180
|
+
assert.deepEqual(imports, [
|
|
181
|
+
{ importPath: "./real", lineNum: 5 },
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
|
|
130
185
|
test("handles empty source", () => {
|
|
131
186
|
const imports = extractRelativeImports("");
|
|
132
187
|
assert.deepEqual(imports, []);
|
|
@@ -810,6 +865,37 @@ describe("runPostExecutionChecks", () => {
|
|
|
810
865
|
}
|
|
811
866
|
});
|
|
812
867
|
|
|
868
|
+
test("does not fail on import-looking strings in task key files", () => {
|
|
869
|
+
tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
|
|
870
|
+
mkdirSync(tempDir, { recursive: true });
|
|
871
|
+
mkdirSync(join(tempDir, "tests"), { recursive: true });
|
|
872
|
+
writeFileSync(join(tempDir, "tests", "real-thing.ts"), "export default true;");
|
|
873
|
+
writeFileSync(
|
|
874
|
+
join(tempDir, "tests", "source-verifier.test.ts"),
|
|
875
|
+
`
|
|
876
|
+
const rewritten = source.replace(
|
|
877
|
+
'import { normalizeZagrebBusinessDeadline } from "./cutoff";',
|
|
878
|
+
'const helper = true;'
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
import realThing from "./real-thing";
|
|
882
|
+
assert.ok(realThing);
|
|
883
|
+
`
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
const task = createTask({
|
|
888
|
+
id: "T03",
|
|
889
|
+
key_files: ["tests/source-verifier.test.ts"],
|
|
890
|
+
});
|
|
891
|
+
const result = runPostExecutionChecks(task, [], tempDir);
|
|
892
|
+
assert.equal(result.status, "pass");
|
|
893
|
+
assert.deepEqual(result.checks, []);
|
|
894
|
+
} finally {
|
|
895
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
813
899
|
test("returns fail status when blocking failure exists", () => {
|
|
814
900
|
tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
|
|
815
901
|
mkdirSync(tempDir, { recursive: true });
|