gentle-pi 2.7.0 → 3.0.0
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 +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -34,7 +34,6 @@ const requiredPaths = [
|
|
|
34
34
|
"assets/agents/sdd-research.md",
|
|
35
35
|
"assets/agents/sdd-spec.md",
|
|
36
36
|
"assets/agents/sdd-status.md",
|
|
37
|
-
"assets/agents/sdd-sync.md",
|
|
38
37
|
"assets/agents/sdd-tasks.md",
|
|
39
38
|
"assets/agents/sdd-verify.md",
|
|
40
39
|
"assets/chains/4r-review.chain.md",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
7
|
+
|
|
8
|
+
// The two Pi homes: `~/.pi/agent` holds Pi's own agent definitions and
|
|
9
|
+
// `~/.pi/gentle-ai` holds everything the Gentle AI commands own. Both are resolved
|
|
10
|
+
// once and shared, because the launch-time profile pin resolver reads the same
|
|
11
|
+
// profiles store the `/gentle:profiles` panel writes: two spellings of the override
|
|
12
|
+
// would silently read two different stores.
|
|
13
|
+
|
|
14
|
+
function isolatedHome(t: test.TestContext): string {
|
|
15
|
+
const home = mkdtempSync(join(tmpdir(), "gentle-pi-config-home-"));
|
|
16
|
+
const previousHome = process.env.HOME;
|
|
17
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
18
|
+
// Isolation must cover POSIX and Windows, the way homedir() resolves each.
|
|
19
|
+
process.env.HOME = home;
|
|
20
|
+
process.env.USERPROFILE = home;
|
|
21
|
+
t.after(() => {
|
|
22
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
23
|
+
else process.env.HOME = previousHome;
|
|
24
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
25
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
26
|
+
rmSync(home, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
return home;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("gentlePiConfigHome honours GENTLE_PI_CONFIG_HOME and otherwise falls back to ~/.pi/gentle-ai", (t) => {
|
|
32
|
+
const home = isolatedHome(t);
|
|
33
|
+
assert.equal(gentlePiConfigHome({ GENTLE_PI_CONFIG_HOME: "/custom/gentle-ai" }), "/custom/gentle-ai");
|
|
34
|
+
assert.equal(gentlePiConfigHome({ GENTLE_PI_CONFIG_HOME: "" }), join(homedir(), ".pi", "gentle-ai"));
|
|
35
|
+
assert.equal(gentlePiConfigHome({}), join(homedir(), ".pi", "gentle-ai"));
|
|
36
|
+
assert.equal(gentlePiConfigHome(), join(homedir(), ".pi", "gentle-ai"));
|
|
37
|
+
assert.equal(
|
|
38
|
+
join(gentlePiConfigHome(), "profiles.json"),
|
|
39
|
+
join(home, ".pi", "gentle-ai", "profiles.json"),
|
|
40
|
+
"the store path is derived from the config home, not from the agent home",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("the config home ignores the Pi agent-home overrides", (t) => {
|
|
45
|
+
const home = isolatedHome(t);
|
|
46
|
+
// PI_CODING_AGENT_DIR and GENTLE_PI_AGENT_HOME move Pi's agent directory only.
|
|
47
|
+
// Reusing that resolver here would point the profiles store at the agent home.
|
|
48
|
+
assert.equal(
|
|
49
|
+
gentlePiConfigHome({ PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/gentle/agent" }),
|
|
50
|
+
join(home, ".pi", "gentle-ai"),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test, { after } from "node:test";
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
setActiveProfile,
|
|
38
38
|
summarizeProfile,
|
|
39
39
|
updateProfile,
|
|
40
|
+
writeJsonFileAtomicallySync,
|
|
40
41
|
writeProfilesFileSync,
|
|
41
42
|
} from "../lib/agent-profiles.ts";
|
|
42
43
|
import type { AgentModelConfig } from "../lib/model-routing-authority.ts";
|
|
@@ -623,6 +624,34 @@ test("writeProfilesFileSync creates missing parent directories", () => {
|
|
|
623
624
|
assert.equal(readProfilesFileResult(path).status, "valid");
|
|
624
625
|
});
|
|
625
626
|
|
|
627
|
+
// The profiles store and the per-repository profile pin both replace their file
|
|
628
|
+
// through this helper, so the interruption guarantee belongs to the helper and not
|
|
629
|
+
// to one store's serializer.
|
|
630
|
+
test("writeJsonFileAtomicallySync writes exact bytes, creates parents, and leaves no temp file behind", () => {
|
|
631
|
+
const path = join(root, "atomic-text", "artifact.json");
|
|
632
|
+
const first = `${JSON.stringify({ kind: "gentle-pi.test", value: 1 }, null, 2)}\n`;
|
|
633
|
+
writeJsonFileAtomicallySync(path, first);
|
|
634
|
+
assert.equal(readFileSync(path, "utf8"), first);
|
|
635
|
+
writeJsonFileAtomicallySync(path, "second\n");
|
|
636
|
+
assert.equal(readFileSync(path, "utf8"), "second\n");
|
|
637
|
+
assert.deepEqual(readdirSync(join(root, "atomic-text")).filter((entry) => entry.includes(".tmp")), []);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("writeJsonFileAtomicallySync skips an identical rewrite so the file is not replaced", () => {
|
|
641
|
+
const path = join(root, "atomic-noop", "artifact.json");
|
|
642
|
+
const text = `${JSON.stringify({ kind: "gentle-pi.test", value: 1 }, null, 2)}\n`;
|
|
643
|
+
writeJsonFileAtomicallySync(path, text);
|
|
644
|
+
const historicalTime = new Date("2000-01-01T00:00:00.000Z");
|
|
645
|
+
utimesSync(path, historicalTime, historicalTime);
|
|
646
|
+
const first = statSync(path).mtimeMs;
|
|
647
|
+
writeJsonFileAtomicallySync(path, text);
|
|
648
|
+
const skipped = statSync(path).mtimeMs;
|
|
649
|
+
assert.equal(readFileSync(path, "utf8"), text);
|
|
650
|
+
assert.equal(skipped, first, "an identical rewrite leaves the file untouched");
|
|
651
|
+
writeJsonFileAtomicallySync(path, "changed\n");
|
|
652
|
+
assert.notEqual(statSync(path).mtimeMs, skipped, "a changed rewrite still replaces the file");
|
|
653
|
+
});
|
|
654
|
+
|
|
626
655
|
test("writeProfilesFileSync replaces the store and leaves no temp file behind", () => {
|
|
627
656
|
const path = join(root, "atomic.json");
|
|
628
657
|
const first = createProfile(emptyProfilesFile(), "alpha", CONFIG);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
parseFrontmatter,
|
|
15
15
|
parseModelRef,
|
|
16
16
|
resolveAgentProfile,
|
|
17
|
+
withPinnedModelProfiles,
|
|
17
18
|
} from "../lib/agents-config.ts";
|
|
18
19
|
import { THINKING_LEVELS } from "../lib/model-routing-authority.ts";
|
|
19
20
|
|
|
@@ -193,6 +194,21 @@ test("parseAgentsConfig applies defaults, validates values, and silently ignores
|
|
|
193
194
|
assert.equal(parseAgentsConfig({ default_mode: "background" }, undefined).defaultMode, AGENT_MODE.BACKGROUND);
|
|
194
195
|
});
|
|
195
196
|
|
|
197
|
+
test("parseAgentsConfig resolves the tool-call stall ceiling above the idle silence budget", () => {
|
|
198
|
+
const defaults = parseAgentsConfig(undefined, undefined);
|
|
199
|
+
assert.equal(defaults.stallTimeoutMs, 4 * 60_000);
|
|
200
|
+
assert.equal(defaults.toolStallTimeoutMs, 30 * 60_000);
|
|
201
|
+
const explicit = parseAgentsConfig({ stall_timeout_ms: 12_000, tool_stall_timeout_ms: 20 * 60_000 }, undefined);
|
|
202
|
+
assert.equal(explicit.stallTimeoutMs, 12_000);
|
|
203
|
+
assert.equal(explicit.toolStallTimeoutMs, 20 * 60_000);
|
|
204
|
+
const projectWins = parseAgentsConfig({ tool_stall_timeout_ms: 20 * 60_000 }, { tool_stall_timeout_ms: 15 * 60_000 });
|
|
205
|
+
assert.equal(projectWins.toolStallTimeoutMs, 15 * 60_000);
|
|
206
|
+
for (const invalid of ["forever", 0, -1, 12.5, null]) {
|
|
207
|
+
assert.equal(parseAgentsConfig({ tool_stall_timeout_ms: invalid }, undefined).toolStallTimeoutMs, 30 * 60_000, `invalid tool_stall_timeout_ms ${String(invalid)} must fall back to the default`);
|
|
208
|
+
}
|
|
209
|
+
assert.equal(parseAgentsConfig({ stall_timeout_ms: 45 * 60_000, tool_stall_timeout_ms: 1_000 }, undefined).toolStallTimeoutMs, 45 * 60_000, "the tool ceiling must never fall below the idle budget");
|
|
210
|
+
});
|
|
211
|
+
|
|
196
212
|
test("resolveAgentProfile prefers the profile, then the definition, then the defaults", () => {
|
|
197
213
|
const config = parseAgentsConfig({ default_model: "openai-codex/gpt-6-astra", default_effort: "medium", model_profiles: { "gentle-ai-explore": { effort: "high" } } }, undefined);
|
|
198
214
|
const explore = parseAgentDefinition(EXPLORER, "/x/explore.md", "global");
|
|
@@ -203,3 +219,31 @@ test("resolveAgentProfile prefers the profile, then the definition, then the def
|
|
|
203
219
|
assert.deepEqual(resolveAgentProfile(bare, config), { model: { provider: "openai-codex", id: "gpt-6-astra" }, thinking: "medium", source: { model: "default", thinking: "default" } });
|
|
204
220
|
assert.deepEqual(resolveAgentProfile(bare, parseAgentsConfig(undefined, undefined)).source, { model: "unresolved", thinking: "unresolved" });
|
|
205
221
|
});
|
|
222
|
+
|
|
223
|
+
test("a pinned profile replaces subagent routing and leaves every other default alone", () => {
|
|
224
|
+
const global = parseAgentsConfig(
|
|
225
|
+
{
|
|
226
|
+
default_model: "openai-codex/gpt-6-astra",
|
|
227
|
+
default_effort: "medium",
|
|
228
|
+
default_mode: "background",
|
|
229
|
+
max_concurrency: 3,
|
|
230
|
+
model_profiles: { explore: { model: "openai-codex/gpt-5.6-terra", effort: "low" } },
|
|
231
|
+
},
|
|
232
|
+
{ model_profiles: { worker: { model: "anthropic/claude-sonnet-5" } } },
|
|
233
|
+
);
|
|
234
|
+
const pinned = withPinnedModelProfiles(global, { worker: { model: "openai/alpha", thinking: "minimal" } });
|
|
235
|
+
assert.deepEqual(pinned.modelProfiles, {
|
|
236
|
+
worker: { model: { provider: "openai", id: "alpha" }, thinking: "minimal" },
|
|
237
|
+
});
|
|
238
|
+
// Materialized global routing must not survive into a pinned repository: that
|
|
239
|
+
// leak is exactly the conflict a per-repository pin exists to remove.
|
|
240
|
+
assert.equal("explore" in pinned.modelProfiles, false);
|
|
241
|
+
// A pin redirects subagent routing only; the orchestrator routing and every
|
|
242
|
+
// operational default stay global.
|
|
243
|
+
assert.deepEqual({ ...pinned, modelProfiles: global.modelProfiles }, global);
|
|
244
|
+
// No pin, and no pin-shaped input, mean today's routing by identity.
|
|
245
|
+
assert.equal(withPinnedModelProfiles(global, undefined), global);
|
|
246
|
+
// A pinned profile that mentions no role still replaces the routing, so a
|
|
247
|
+
// repository can pin "everything inherits" without touching the global store.
|
|
248
|
+
assert.deepEqual(withPinnedModelProfiles(global, {}).modelProfiles, {});
|
|
249
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
3
|
-
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import test, { after } from "node:test";
|
|
7
|
-
import {
|
|
6
|
+
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
8
7
|
import { applyTaskEvent, emptyThread, TASK_EVENT, TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
9
8
|
|
|
10
9
|
// Gentle Agents history: JSON per task, async, lazy, pruned by count.
|
|
@@ -13,13 +12,6 @@ const root = mkdtempSync(join(tmpdir(), "gentle-agents-history-"));
|
|
|
13
12
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
14
13
|
const dir = join(root, "tasks");
|
|
15
14
|
|
|
16
|
-
function orphanLock(lockDir: string, id: string): void {
|
|
17
|
-
const moduleUrl = new URL("../lib/agents-history.ts", import.meta.url).href;
|
|
18
|
-
const source = `import { acquireTaskLock } from ${JSON.stringify(moduleUrl)}; const [dir, id] = process.argv.slice(-2); acquireTaskLock(dir, id);`;
|
|
19
|
-
const child = spawnSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "-e", source, lockDir, id], { encoding: "utf8" });
|
|
20
|
-
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
15
|
function task(id: string, createdAt: number): TaskRecord {
|
|
24
16
|
return { id, agent: "explore", mode: "task", prompt: "p", label: "p", cwd: "/r", parentSessionId: "s", status: TASK_STATUS.COMPLETED, createdAt, startedAt: createdAt, endedAt: createdAt + 5, model: "m", thinking: undefined, sessionPath: null, error: null, result: "ok", lastStep: "done", lastActivityAt: createdAt, turns: 1, toolCalls: 0, tokens: 10, cost: 0.01 };
|
|
25
17
|
}
|
|
@@ -41,20 +33,6 @@ test("saveTask writes a task with its thread and loadStoredTask reads it back",
|
|
|
41
33
|
assert.deepEqual(readdirSync(dir), ["a1.json"], "no temp file is left behind");
|
|
42
34
|
});
|
|
43
35
|
|
|
44
|
-
test("task reconciliation elections bypass dead candidates and fail closed for active or ambiguous candidates", () => {
|
|
45
|
-
const lockDir = join(root, "task-locks"), token = "11111111-1111-4111-8111-111111111111";
|
|
46
|
-
const held = acquireTaskLock(lockDir, "busy");
|
|
47
|
-
assert.match(held.path, /busy\.reconcile\.[0-9a-f-]+$/);
|
|
48
|
-
assert.throws(() => acquireTaskLock(lockDir, "busy"), /busy|active|ambiguous/i); held.release();
|
|
49
|
-
orphanLock(lockDir, "dead");
|
|
50
|
-
const deadName = readdirSync(lockDir).find(name => name.startsWith("dead.reconcile."))!;
|
|
51
|
-
const bypassed = acquireTaskLock(lockDir, "dead");
|
|
52
|
-
assert.notEqual(bypassed.path, join(lockDir, deadName)); assert.ok(!readdirSync(lockDir).includes(deadName)); bypassed.release();
|
|
53
|
-
const malformed = join(lockDir, `malformed.reconcile.${token}`); writeFileSync(malformed, "not-json");
|
|
54
|
-
assert.throws(() => acquireTaskLock(lockDir, "malformed"), /busy|active|ambiguous|malformed/i);
|
|
55
|
-
const foreign = join(lockDir, `foreign.reconcile.${token}`); writeFileSync(foreign, JSON.stringify({ schema: "gentle-pi.task-reconciliation-lock/v1", taskId: "foreign", token, pid: process.pid, host: "foreign-host" }));
|
|
56
|
-
assert.throws(() => acquireTaskLock(lockDir, "foreign"), /busy|active|ambiguous|foreign/i);
|
|
57
|
-
});
|
|
58
36
|
test("loadHistory skips broken files, sorts newest first, and pruneHistory keeps the newest N", async () => {
|
|
59
37
|
await saveTask(dir, task("b2", 3000), emptyThread());
|
|
60
38
|
await saveTask(dir, task("c3", 2000), emptyThread());
|
|
@@ -74,3 +52,13 @@ test("TaskStore.restore adds a stored task without clobbering a live one", () =>
|
|
|
74
52
|
assert.equal(store.restore({ ...task("r1", 1000), result: "other" }, emptyThread()), false);
|
|
75
53
|
assert.equal(store.get("r1")?.result, "ok");
|
|
76
54
|
});
|
|
55
|
+
|
|
56
|
+
test("retired remediation payloads remain readable and are never automatically pruned or replayed", async () => {
|
|
57
|
+
const legacyDir = join(root, "legacy-remediation");
|
|
58
|
+
const legacy = { ...task("legacy", 1), sddRemediation: { acquire: { requestId: "old" }, token: "historical", settlement: { state: "complete" } } };
|
|
59
|
+
await saveTask(legacyDir, legacy, emptyThread());
|
|
60
|
+
const before = readFileSync(join(legacyDir, "legacy.json"), "utf8");
|
|
61
|
+
assert.equal(await pruneHistory(legacyDir, 0), 0);
|
|
62
|
+
assert.deepEqual((await loadStoredTask(legacyDir, "legacy"))?.task, JSON.parse(before).task);
|
|
63
|
+
assert.equal(readFileSync(join(legacyDir, "legacy.json"), "utf8"), before);
|
|
64
|
+
});
|