feat(job-array-one-based-index): control by filter with element selections (task 7.5)

Filter-based ControlJob validates a bracketed -J value before fetching
(400 with the parser's message; unbracketed names, including commands
with spaces, unchanged). FetchJobList evaluates bracketed job_name
entries through the selection predicates instead of unevaluated
compound conditions, so -J 'foo[1]' acts on element 1 of every array
named foo and never on ordinary jobs of that name. The most-recent-job
rule applies only without an element number and without all-jobs, and
treats the element of a one-element array like an ordinary job
(isSingleElementArray, a documented approximation). The old
index-section parser is deleted. Completes bug-1898 on the owned paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 12:40:09 +00:00
parent db16b565dd
commit 40917a01f0
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
5 changed files with 251 additions and 134 deletions

View file

@ -1234,3 +1234,152 @@ func TestJobHandler_ControlValidThenInvalid(t *testing.T) {
}
}
}
// ========== Control by filter with element selections (task 7.5) ==========
// controlFilterKill posts a kill through the filter-based ControlJob with
// the job-name filter name (the -J value as the client sends it).
func (f *handlerFixture) controlFilterKill(name string) *httptest.ResponseRecorder {
f.t.Helper()
return f.controlByFilter(models.JobControlRequest{
Filter: models.JobControlFilter{JobName: name},
Action: "kill",
})
}
// TestJobHandler_ControlFilterRejectsBadSelection (5.7, 5.16, 5.5): a
// bracketed -J value the name parser rejects returns 400 with its specific
// message before any job is fetched, and dispatches nothing.
func TestJobHandler_ControlFilterRejectsBadSelection(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 40<<15
elems := f.seedArrayElements(X, "foo", "RUN", 1, 2)
assertSeeded(t, uint64(elems[0].Unique_JobID))
cases := []struct{ name, want string }{
{"foo[40000]", "element numbers must be at most"},
{"foo[3-1]", "reversed"},
{"foo[a]", "invalid"},
{"foo [1]", "invalid"},
{"foo[1", "invalid"},
}
for _, c := range cases {
f.dispatched = nil
assert400(t, "-J "+c.name, f.controlFilterKill(c.name), c.want)
if got := f.dispatchedIDs(); len(got) != 0 {
t.Errorf("-J %s: dispatched %v, want none", c.name, got)
}
}
}
// TestJobHandler_ControlFilterEmptyNameSelectsNothing (design ApiMatch):
// -J '[1]' (control sends eq) matches no row, not the unnamed array.
func TestJobHandler_ControlFilterEmptyNameSelectsNothing(t *testing.T) {
f := newHandlerFixture(t)
Z := handlerTestArrayBase + 41<<15
elems := f.seedArrayElements(Z, "", "RUN", 1)
assertSeeded(t, uint64(elems[0].Unique_JobID))
w := f.controlFilterKill("[1]")
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
}
if got := f.dispatchedIDs(); len(got) != 0 {
t.Errorf("-J [1] dispatched %v, want none", got)
}
}
// TestJobHandler_ControlFilterMostRecentOneElementArray (5.10): with no
// element number in the filter, when the newest matching job is the element
// of a one-element array, only that element is acted on.
func TestJobHandler_ControlFilterMostRecentOneElementArray(t *testing.T) {
f := newHandlerFixture(t)
O := handlerTestOrdinaryBase + 50
f.seedOrdinaryJob(O, "foo", "RUN")
A := handlerTestArrayBase + 42<<15
a1 := backend.MakeElementID(A, 1)
f.seedArrayElements(A, "foo", "RUN", 1)
assertSeeded(t, O, a1)
r := decodeControlReply(t, f.controlFilterKill("foo"))
assertExactIDs(t, "-J foo dispatched", f.dispatchedIDs(), []uint64{a1})
assertExactIDs(t, "-J foo accepted", itemIDs(t, r.Accepted), []uint64{a1})
// all-jobs: the rule does not apply, every matching job is acted on
f.dispatched = nil
w := f.controlByFilter(models.JobControlRequest{
Filter: models.JobControlFilter{JobName: "foo", AllJobs: true},
Action: "kill",
})
decodeControlReply(t, w)
assertExactIDs(t, "-J foo -a dispatched", f.dispatchedIDs(), []uint64{O, a1})
}
// TestJobHandler_ControlFilterElementSelection (5.2): -J 'foo[1]' acts on
// exactly element 1 of every array named foo -- both one-element arrays and
// element 1 of a larger one -- never on an ordinary job named foo, another
// element, or another name, whatever the newest row is.
func TestJobHandler_ControlFilterElementSelection(t *testing.T) {
f := newHandlerFixture(t)
O := handlerTestOrdinaryBase + 51
f.seedOrdinaryJob(O, "foo", "RUN")
A := handlerTestArrayBase + 43<<15
B := handlerTestArrayBase + 44<<15
C := handlerTestArrayBase + 45<<15
D := handlerTestArrayBase + 46<<15
f.seedArrayElements(A, "foo", "RUN", 1)
f.seedArrayElements(B, "foo", "RUN", 1)
f.seedArrayElements(C, "foo", "RUN", 1, 2)
f.seedArrayElements(D, "bar", "RUN", 1)
a1, b1, c1 := backend.MakeElementID(A, 1), backend.MakeElementID(B, 1), backend.MakeElementID(C, 1)
assertSeeded(t, O, a1, b1, c1, backend.MakeElementID(C, 2), backend.MakeElementID(D, 1))
r := decodeControlReply(t, f.controlFilterKill("foo[1]"))
assertExactIDs(t, "-J foo[1] dispatched", f.dispatchedIDs(), []uint64{a1, b1, c1})
assertExactIDs(t, "-J foo[1] accepted", itemIDs(t, r.Accepted), []uint64{a1, b1, c1})
// the range form 1-1 selects the same element 1 of every array named foo
f.dispatched = nil
r = decodeControlReply(t, f.controlFilterKill("foo[1-1]"))
assertExactIDs(t, "-J foo[1-1] dispatched", f.dispatchedIDs(), []uint64{a1, b1, c1})
}
// TestJobHandler_ControlFilterElementSelectionNewestSingleElement (5.2,
// 5.10): when the most recent job named foo is the element of a one-element
// array, -J 'foo[1]' still acts on element 1 of every array named foo (the
// most-recent rule applies only to filters without an element number),
// while -J 'foo' acts on that most recent element alone.
func TestJobHandler_ControlFilterElementSelectionNewestSingleElement(t *testing.T) {
f := newHandlerFixture(t)
O := handlerTestOrdinaryBase + 53
f.seedOrdinaryJob(O, "foo", "RUN")
A := handlerTestArrayBase + 47<<15
B := handlerTestArrayBase + 48<<15 // highest ID: the newest row
f.seedArrayElements(A, "foo", "RUN", 1)
f.seedArrayElements(B, "foo", "RUN", 1)
a1, b1 := backend.MakeElementID(A, 1), backend.MakeElementID(B, 1)
assertSeeded(t, O, a1, b1)
r := decodeControlReply(t, f.controlFilterKill("foo[1]"))
assertExactIDs(t, "-J foo[1] dispatched", f.dispatchedIDs(), []uint64{a1, b1})
assertExactIDs(t, "-J foo[1] accepted", itemIDs(t, r.Accepted), []uint64{a1, b1})
f.dispatched = nil
r = decodeControlReply(t, f.controlFilterKill("foo"))
assertExactIDs(t, "-J foo dispatched", f.dispatchedIDs(), []uint64{b1})
assertExactIDs(t, "-J foo accepted", itemIDs(t, r.Accepted), []uint64{b1})
}
// TestJobHandler_ControlFilterPlainNameWithSpaces: an unnamed job carries
// its command as its name; kill -J 'sleep 100' keeps working (no brackets,
// no selection parsing).
func TestJobHandler_ControlFilterPlainNameWithSpaces(t *testing.T) {
f := newHandlerFixture(t)
O := handlerTestOrdinaryBase + 52
f.seedOrdinaryJob(O, "sleep 100", "RUN")
f.seedOrdinaryJob(O+1, "sleep 200", "RUN")
r := decodeControlReply(t, f.controlFilterKill("sleep 100"))
assertExactIDs(t, "-J 'sleep 100' dispatched", f.dispatchedIDs(), []uint64{O})
assertExactIDs(t, "-J 'sleep 100' accepted", itemIDs(t, r.Accepted), []uint64{O})
}