Files
stack/agents/rocko/work/queue-56/out/round2-delta.diff
T

157 lines
8.4 KiB
Diff

diff --git tree/agents/rocko/work/queue-56/delta-check.sh r2/agents/rocko/work/queue-56/delta-check.sh
index 69224ea..a502d1d 100755
--- tree/agents/rocko/work/queue-56/delta-check.sh
+++ r2/agents/rocko/work/queue-56/delta-check.sh
@@ -59,4 +59,7 @@ if [ "$(id -u)" != 0 ]; then
hdr "runs directory unreadable: list"; run_case unreadable list
hdr "runs directory unreadable: show"; run_case unreadable show "$A"
chmod 755 "$D/runs"
+ D="$(root run-unreadable)"; mkdir -p "$D/runs/$A"; printf '%s\n' "$RESULT" > "$D/runs/$A/result.json"; chmod 000 "$D/runs/$A"
+ hdr "run directory unreadable: show"; run_case run-unreadable show "$A"
+ chmod 755 "$D/runs/$A"
fi
diff --git tree/packages/runs/README.md r2/packages/runs/README.md
index 84230a1..8fd7808 100644
--- tree/packages/runs/README.md
+++ r2/packages/runs/README.md
@@ -40,7 +40,11 @@ printed them.
- a run document resolving outside reads as `null`;
- `active.json` or `activation-log.jsonl` resolving outside refuses.
- A link that stays inside the data root is followed.
+ Only a path that exists can resolve. A link with nothing behind it reads
+ as missing wherever it points. A `state/` link out of the data root to a
+ path that doesn't exist reads as no release and an empty log, and a
+ `runs/` link like that lists nothing. A link that stays inside the data
+ root is followed.
- **Run documents are JSON objects or `null`.** A document that is
missing, unreadable, not JSON, or JSON but not an object (`null`, `5`,
`[]`) reads as `null`. Run records are never validated beyond that,
@@ -49,11 +53,14 @@ printed them.
shape `scripts/release.sh` writes: exactly `pointerVersion` 1 and
non-empty strings `release`, `imageTag` and `activatedAt`. Anything else
refuses with exit code 2 rather than being guessed at.
-- **The activation log is lenient per line.** An entry needs string `at`,
- `event`, `release` and `imageTag`, an optional string `note`, and no
- other keys. Other lines are counted in `malformed` and skipped, the way
- `release.sh rollback` skips them. Blank lines aren't counted. `last`
- must be a positive integer and keeps the newest entries.
+- **The activation log is read per line.** One bad line never refuses
+ the log. An entry needs string `at`, `event`, `release` and `imageTag`,
+ an optional string `note`, and no other keys. A line that isn't JSON,
+ or is JSON with another shape, is counted in `malformed` and skipped.
+ That is stricter than `release.sh rollback`, which skips only lines that
+ aren't JSON and would act on an entry with an extra key or a non-string
+ field. Blank lines aren't counted. `last` must be a positive integer and
+ keeps the newest entries.
- **Errors.** Every refusal is a `RunsError` with `exitCode` 2 (invalid
data) or 4 (a file or environment problem), matching
`mosaic-task.mjs`. A missing data root, `runs/` or `state/` file is not
@@ -78,6 +85,7 @@ output changes on purpose:
| `task.json` or `mission.json` is JSON but not an object | `show` printed its snapshot line | snapshot line omitted |
| run is a regular file | `show` crashed | `run not found`, exit 4 |
| `runs/` unreadable | `list` printed nothing | refuses, exit 4 |
+| run directory unreadable (mode 000) | `show` printed two lines, then crashed, exit 1 | refuses with EACCES, exit 4 |
| link loop or a permission error resolving a run | `run not found` | refuses with the error code, exit 4 |
`agents/rocko/work/queue-56/delta-check.sh` prints the before and after
diff --git tree/packages/runs/src/state.mjs r2/packages/runs/src/state.mjs
index a92c871..af5cc92 100644
--- tree/packages/runs/src/state.mjs
+++ r2/packages/runs/src/state.mjs
@@ -60,10 +60,11 @@ function logEntry(line) {
return entry;
}
-// The activation log as { entries, malformed }, oldest first. Lines that
-// aren't a well-formed entry are counted in malformed and skipped, the way
-// release.sh rollback skips them. A missing log is empty. With last, only
-// the newest last well-formed entries are returned.
+// The activation log as { entries, malformed }, oldest first. A line that
+// isn't JSON, or is JSON but not the entry shape release.sh writes, is
+// counted in malformed and skipped. This is stricter than release.sh
+// rollback, which skips only lines that aren't JSON. A missing log is empty.
+// With last, only the newest last well-formed entries are returned.
export function readActivationLog(dataRoot, { last } = {}) {
if (last !== undefined && (!Number.isInteger(last) || last < 1)) {
throw new RunsError(`last must be a positive integer (got ${JSON.stringify(last)})`);
diff --git tree/packages/runs/tests/runs.test.mjs r2/packages/runs/tests/runs.test.mjs
index 85838e5..a652f62 100644
--- tree/packages/runs/tests/runs.test.mjs
+++ r2/packages/runs/tests/runs.test.mjs
@@ -216,6 +216,15 @@ test("listRunIds sorts whatever order the directory returns", (t) => {
assert.deepEqual(listRunIds(dataRoot), [A, B]);
});
+// list has always shown any r- name, including ones show refuses as ids.
+test("listRunRecords lists an r- name that isn't a valid run id, as before", (t) => {
+ const { dataRoot } = scratch(t);
+ write(path.join(dataRoot, "runs", "r-.bad", "result.json"), {});
+ assert.equal(isRunId("r-.bad"), false);
+ assert.deepEqual(listRunIds(dataRoot), ["r-.bad"]);
+ assert.deepEqual(listRunRecords(dataRoot), [{ runId: "r-.bad", result: {} }]);
+});
+
test("the readers write nothing", (t) => {
const { dataRoot } = scratch(t);
const runs = path.join(dataRoot, "runs");
diff --git tree/packages/runs/tests/state.test.mjs r2/packages/runs/tests/state.test.mjs
index 80ab5ea..4b126f3 100644
--- tree/packages/runs/tests/state.test.mjs
+++ r2/packages/runs/tests/state.test.mjs
@@ -72,6 +72,13 @@ test("a state directory linked out of the data root refuses", (t) => {
assert.throws(() => readActivePointer(dataRoot), /state\/active.json resolves outside the data root/);
});
+test("a state directory linked out to nothing reads as no release and an empty log", (t) => {
+ const { dataRoot, outside } = scratch(t);
+ fs.symlinkSync(path.join(outside, "absent"), path.join(dataRoot, "state"));
+ assert.equal(readActivePointer(dataRoot), null);
+ assert.deepEqual(readActivationLog(dataRoot), { entries: [], malformed: 0 });
+});
+
test("a missing log is empty", (t) => {
const { dataRoot } = scratch(t);
assert.deepEqual(readActivationLog(dataRoot), { entries: [], malformed: 0 });
@@ -95,11 +102,13 @@ test("the log reads oldest first and counts malformed lines", (t) => {
{ ...good[1], extra: true },
{ ...good[1], at: 5 },
{ ...good[1], note: null },
+ { at: good[1].at, event: good[1].event, release: good[1].release },
+ { ...good[1], release: 5 },
good[2],
good[3],
]);
- assert.deepEqual(readActivationLog(dataRoot), { entries: good, malformed: 5 });
- assert.deepEqual(readActivationLog(dataRoot, { last: 2 }), { entries: good.slice(2), malformed: 5 });
+ assert.deepEqual(readActivationLog(dataRoot), { entries: good, malformed: 7 });
+ assert.deepEqual(readActivationLog(dataRoot, { last: 2 }), { entries: good.slice(2), malformed: 7 });
assert.deepEqual(readActivationLog(dataRoot, { last: 99 }).entries, good);
});
diff --git tree/packages/runs/tests/task-cli.test.mjs r2/packages/runs/tests/task-cli.test.mjs
index b5fafd4..ed13ecd 100644
--- tree/packages/runs/tests/task-cli.test.mjs
+++ r2/packages/runs/tests/task-cli.test.mjs
@@ -92,3 +92,20 @@ test("show refuses a run linked out of the data root; list reports it unknown",
assert.equal(list.status, 0, list.stderr);
assert.equal(list.stdout, `${A} unknown task=- ws=- session=-\n`);
});
+
+test("list shows an r- name that isn't a valid run id, as before", (t) => {
+ const { dataRoot } = scratch(t);
+ write(path.join(dataRoot, "runs", "r-.bad", "result.json"), RESULT);
+ const out = task(t, dataRoot, "list");
+ assert.equal(out.status, 0, out.stderr);
+ assert.equal(out.stdout, "r-.bad succeeded task=t-one ws=- session=-\n");
+});
+
+test("show refuses an invalid id before reading the config", (t) => {
+ const { dir } = scratch(t);
+ const env = { ...process.env, MOSAIC_CONFIG: path.join(dir, "missing.json") };
+ delete env.NODE_TEST_CONTEXT;
+ const proc = spawnSync(process.execPath, [path.join(ROOT, "scripts", "mosaic-task.mjs"), "show", "../x"], { cwd: ROOT, env, encoding: "utf8" });
+ assert.equal(proc.status, 4, proc.stderr);
+ assert.equal(proc.stderr, 'mosaic-task: invalid run id: "../x" (expected r-<id>)\n');
+});