+50
-1
@@ -529,6 +529,52 @@ function retryRun(runId) {
|
|||||||
runTask(tempTaskFile, { retriedFrom: runId });
|
runTask(tempTaskFile, { retriedFrom: runId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pruneRuns(args) {
|
||||||
|
const resolved = loadConfig();
|
||||||
|
const root = runsRoot(resolved);
|
||||||
|
let keep = 50;
|
||||||
|
let apply = false;
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg === "--yes") apply = true;
|
||||||
|
else if (arg === "--keep") fail(4, "prune: --keep requires a value");
|
||||||
|
else if (arg.startsWith("--keep=")) {
|
||||||
|
keep = Number(arg.slice("--keep=".length));
|
||||||
|
if (!Number.isInteger(keep) || keep < 1) fail(4, `--keep must be a positive integer (got ${arg.slice(7)})`);
|
||||||
|
} else fail(4, `unknown prune option: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = [];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(root, { withFileTypes: true })
|
||||||
|
.filter((e) => e.name.startsWith("r-") && e.isDirectory() && !e.isSymbolicLink())
|
||||||
|
.map((e) => e.name)
|
||||||
|
.sort();
|
||||||
|
} catch {
|
||||||
|
// No runs yet.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length <= keep) {
|
||||||
|
process.stdout.write(`prune: ${entries.length} run(s) present, keep=${keep} -> nothing to prune\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doomed = entries.slice(0, entries.length - keep); // oldest first
|
||||||
|
if (!apply) {
|
||||||
|
process.stdout.write(`prune (dry-run): would remove ${doomed.length} oldest run(s), keep ${entries.length - doomed.length}:\n`);
|
||||||
|
for (const id of doomed) process.stdout.write(` would remove: ${id}\n`);
|
||||||
|
process.stdout.write("prune: re-run with --yes to apply\n");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receipt = path.join(root, ".pruned.log");
|
||||||
|
for (const id of doomed) {
|
||||||
|
fs.rmSync(path.join(root, id), { recursive: true, force: true });
|
||||||
|
fs.appendFileSync(receipt, `${JSON.stringify({ at: new Date().toISOString(), event: "pruned", runId: id })}\n`);
|
||||||
|
}
|
||||||
|
process.stdout.write(`prune: removed ${doomed.length} run(s), kept ${keep}; receipt: ${receipt}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
const operation = process.argv[2];
|
const operation = process.argv[2];
|
||||||
const target = process.argv[3];
|
const target = process.argv[3];
|
||||||
|
|
||||||
@@ -552,10 +598,13 @@ switch (operation) {
|
|||||||
case "list":
|
case "list":
|
||||||
listRuns();
|
listRuns();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
case "prune":
|
||||||
|
pruneRuns(process.argv.slice(3));
|
||||||
|
break;
|
||||||
case "retry":
|
case "retry":
|
||||||
if (!target) fail(4, "usage: mosaic-task.mjs retry <runId>");
|
if (!target) fail(4, "usage: mosaic-task.mjs retry <runId>");
|
||||||
retryRun(target);
|
retryRun(target);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | show | list | retry)`);
|
fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | show | list | retry | prune)`);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-10
@@ -42,10 +42,6 @@ latest_reason() {
|
|||||||
[ -n "$latest" ] && node -e 'try{const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));console.log(r.reason??"")}catch{console.log("")}' "$latest/result.json" 2>/dev/null
|
[ -n "$latest" ] && node -e 'try{const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));console.log(r.reason??"")}catch{console.log("")}' "$latest/result.json" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
check() {
|
|
||||||
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
CONFIG="$SANDBOX/config.json"
|
CONFIG="$SANDBOX/config.json"
|
||||||
DATA_ROOT="$SANDBOX/data"
|
DATA_ROOT="$SANDBOX/data"
|
||||||
mkdir -p "$DATA_ROOT"
|
mkdir -p "$DATA_ROOT"
|
||||||
@@ -108,6 +104,37 @@ $TASK validate "$SANDBOX/ok.json" >/dev/null 2>&1
|
|||||||
M2=$(stat -c %Y "$SANDBOX/ok.json")
|
M2=$(stat -c %Y "$SANDBOX/ok.json")
|
||||||
[ "$M1" = "$M2" ] && check "validation does not modify the task file" 0 || check "validation does not modify the task file" 1
|
[ "$M1" = "$M2" ] && check "validation does not modify the task file" 0 || check "validation does not modify the task file" 1
|
||||||
|
|
||||||
|
# ---------- retention: prune (deterministic, no Docker) ----------
|
||||||
|
mkdir -p "$SANDBOX/data"
|
||||||
|
cat > "$SANDBOX/prune-config.json" <<EOF
|
||||||
|
{"configVersion":1,"environment":"development","dataRoot":"$SANDBOX/data","execution":{"backend":"docker","provider":"zai","model":"m"}}
|
||||||
|
EOF
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
D="$SANDBOX/data/runs/r-20260903T0100_0${i}Z-suite00$i"
|
||||||
|
mkdir -p "$D"
|
||||||
|
printf '{"runVersion":1,"runId":"r-20260903T0100_0%sZ-suite00%s","taskId":"t","status":"succeeded"}' "$i" "$i" > "$D/result.json"
|
||||||
|
done
|
||||||
|
mkdir -p "$SANDBOX/data/sessions/sentinel" "$SANDBOX/data/workspaces/sentinel"
|
||||||
|
|
||||||
|
expect_exit "prune dry-run exits 0" 0 -- env MOSAIC_CONFIG="$SANDBOX/prune-config.json" node scripts/mosaic-task.mjs prune
|
||||||
|
[ "$(ls "$SANDBOX/data/runs" | grep -c '^r-')" -eq 5 ] \
|
||||||
|
&& check "dry-run deleted nothing" 0 || check "dry-run deleted nothing" 1
|
||||||
|
|
||||||
|
expect_exit "prune --keep=2 --yes removes oldest" 0 -- env MOSAIC_CONFIG="$SANDBOX/prune-config.json" node scripts/mosaic-task.mjs prune --keep=2 --yes
|
||||||
|
[ "$(ls "$SANDBOX/data/runs" | grep -c '^r-')" -eq 2 ] \
|
||||||
|
&& check "kept exactly 2 newest runs" 0 || check "kept exactly 2 newest runs" 1
|
||||||
|
NEWEST="r-20260903T0100_05Z-suite005"
|
||||||
|
[ -d "$SANDBOX/data/runs/$NEWEST" ] \
|
||||||
|
&& check "newest run kept, oldest pruned" 0 || check "newest run kept, oldest pruned" 1
|
||||||
|
[ -f "$SANDBOX/data/runs/.pruned.log" ] \
|
||||||
|
&& [ "$(grep -c 'pruned' "$SANDBOX/data/runs/.pruned.log")" -eq 3 ] \
|
||||||
|
&& check "append-only receipt written (3 entries)" 0 \
|
||||||
|
|| check "append-only receipt written (3 entries)" 1
|
||||||
|
[ -d "$SANDBOX/data/sessions/sentinel" ] && [ -d "$SANDBOX/data/workspaces/sentinel" ] \
|
||||||
|
&& check "sessions/workspaces untouched by prune" 0 \
|
||||||
|
|| check "sessions/workspaces untouched by prune" 1
|
||||||
|
expect_exit "prune with invalid keep exits 4" 4 -- env MOSAIC_CONFIG="$SANDBOX/prune-config.json" node scripts/mosaic-task.mjs prune --keep=0 --yes
|
||||||
|
|
||||||
# ---------- adapter seam: deterministic mock cases (Docker, no provider) ----------
|
# ---------- adapter seam: deterministic mock cases (Docker, no provider) ----------
|
||||||
if docker info >/dev/null 2>&1; then
|
if docker info >/dev/null 2>&1; then
|
||||||
good_task "$SANDBOX/ok.json"
|
good_task "$SANDBOX/ok.json"
|
||||||
@@ -244,12 +271,6 @@ dump_latest_run() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_reason() {
|
|
||||||
local latest
|
|
||||||
latest="$(ls -dt "$SANDBOX/data/runs"/r-* 2>/dev/null | head -1)"
|
|
||||||
[ -n "$latest" ] && node -e 'try{const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));console.log(r.reason??"")}catch{console.log("")}' "$latest/result.json" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
if docker info >/dev/null 2>&1; then
|
if docker info >/dev/null 2>&1; then
|
||||||
RUNS1=$(ls "$DATA_ROOT/runs" 2>/dev/null | wc -l)
|
RUNS1=$(ls "$DATA_ROOT/runs" 2>/dev/null | wc -l)
|
||||||
if scripts/run-task.sh run "$SANDBOX/ok.json" >/dev/null 2>&1; then
|
if scripts/run-task.sh run "$SANDBOX/ok.json" >/dev/null 2>&1; then
|
||||||
|
|||||||
Reference in New Issue
Block a user