aravind changes
This commit is contained in:
695
go-api/internal/owliver/suggest_test.go
Normal file
695
go-api/internal/owliver/suggest_test.go
Normal file
@@ -0,0 +1,695 @@
|
||||
package owliver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/krow/krow-backend/go-api/internal/definition"
|
||||
"github.com/krow/krow-backend/go-api/internal/domain"
|
||||
)
|
||||
|
||||
// These tests need no database and no server: the catalogue is static and the
|
||||
// ranking is a pure function of (page, query, role). That is the property worth
|
||||
// protecting — an endpoint the panel calls on every keystroke should be
|
||||
// testable at the speed of a string comparison.
|
||||
|
||||
// intents is the ids Suggest returned, in order.
|
||||
func intents(got []Suggestion) []string {
|
||||
out := make([]string, len(got))
|
||||
for i, s := range got {
|
||||
out[i] = s.Intent
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ask is Suggest for an admin, the role the Owliver panel is placed for.
|
||||
func ask(page, query string) []Suggestion {
|
||||
return Suggest(page, query, domain.RoleAdmin)
|
||||
}
|
||||
|
||||
/* ── Control Center ─────────────────────────────────────────────────────── */
|
||||
|
||||
func TestControlCenterSuggestions(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
want []string
|
||||
}{
|
||||
{"attention", "attention", []string{"attention-required"}},
|
||||
{"pipeline", "pipeline", []string{"pipeline-health"}},
|
||||
{"health", "health", []string{"platform-health"}},
|
||||
{"recommendation", "what should i do", []string{"recommendations"}},
|
||||
|
||||
// A question about a subject this page does not hold. The Control
|
||||
// Center reads the platform, not the training library.
|
||||
{"irrelevant", "forklift certification renewal", nil},
|
||||
{"empty", "", nil},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := intents(ask("control-center", c.query))
|
||||
if len(c.want) == 0 {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("query %q: want no suggestions, got %v", c.query, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Fatalf("query %q: got %v, want %v", c.query, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Positions ──────────────────────────────────────────────────────────── */
|
||||
|
||||
func TestPositionsSuggestions(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
want []string
|
||||
}{
|
||||
// The page's own noun offers the page's readings, in declaration order.
|
||||
{"position", "position", []string{"position-drafts", "position-strength", "positions-attention"}},
|
||||
|
||||
// Pipeline on Positions is a question about the ROLES: which one is
|
||||
// converting, and where it is stuck. Two answers, not three — the third
|
||||
// slot is left empty rather than filled with the page's list of people
|
||||
// waiting, which is a different question wearing a nearby word.
|
||||
{"pipeline", "pipeline", []string{"position-strength", "pipeline-health"}},
|
||||
|
||||
{"attention", "attention", []string{"positions-attention"}},
|
||||
{"risk", "risk", []string{"positions-attention"}},
|
||||
{"drafts", "draft", []string{"position-drafts"}},
|
||||
{"fill first", "what should i fill first", []string{"hiring-priority"}},
|
||||
|
||||
// Attendance is a workforce reading and belongs to another surface. It
|
||||
// must not fall through to this page's default report.
|
||||
{"irrelevant", "attendance last week", nil},
|
||||
{"empty", "", nil},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := intents(ask("positions", c.query))
|
||||
if len(c.want) == 0 {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("query %q: want no suggestions, got %v", c.query, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Fatalf("query %q: got %v, want %v", c.query, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Candidates ─────────────────────────────────────────────────────────── */
|
||||
|
||||
func TestCandidatesSuggestions(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
want []string
|
||||
}{
|
||||
{"candidate", "candidate", []string{"candidates-attention", "top-candidates", "interview-ready"}},
|
||||
{"score", "score", []string{"top-candidates", "screening-gaps"}},
|
||||
{"interview", "interview", []string{"interview-ready"}},
|
||||
{"decision", "decision", []string{"candidates-attention", "candidate-risk"}},
|
||||
{"pipeline", "pipeline", []string{"pipeline-summary"}},
|
||||
{"risk", "risk", []string{"candidate-risk"}},
|
||||
|
||||
{"irrelevant", "payroll export", nil},
|
||||
{"empty", "", nil},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := intents(ask("candidates", c.query))
|
||||
if len(c.want) == 0 {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("query %q: want no suggestions, got %v", c.query, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Fatalf("query %q: got %v, want %v", c.query, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Page context is the first filter ───────────────────────────────────── */
|
||||
|
||||
// One keyword, every page: the answers must differ, and none may name a
|
||||
// reading belonging to another surface.
|
||||
func TestSameKeywordDiffersByPage(t *testing.T) {
|
||||
const query = "pipeline"
|
||||
|
||||
seen := map[string][]string{}
|
||||
for _, page := range Pages() {
|
||||
got := intents(ask(page, query))
|
||||
if len(got) == 0 {
|
||||
continue
|
||||
}
|
||||
seen[page] = got
|
||||
|
||||
for _, id := range got {
|
||||
if !declaredOn(page, id) {
|
||||
t.Fatalf("page %q returned %q, which it does not declare", page, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("%q matched on %d pages; the comparison needs at least two", query, len(seen))
|
||||
}
|
||||
if reflect.DeepEqual(seen["positions"], seen["candidates"]) {
|
||||
t.Fatalf("positions and candidates both answered %q with %v", query, seen["positions"])
|
||||
}
|
||||
// The pipeline reading on Candidates is the candidates' own.
|
||||
if !reflect.DeepEqual(seen["candidates"], []string{"pipeline-summary"}) {
|
||||
t.Fatalf("candidates answered %q with %v", query, seen["candidates"])
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown page is not this package's error to raise — the service refuses it
|
||||
// during parsing. Reached directly it answers empty rather than borrowing
|
||||
// another page's readings.
|
||||
func TestUnknownPageIsEmpty(t *testing.T) {
|
||||
for _, page := range []string{"", "nowhere", "POSITIONS", "settings"} {
|
||||
if got := ask(page, "pipeline"); len(got) != 0 {
|
||||
t.Fatalf("page %q: got %v, want none", page, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func declaredOn(page, id string) bool {
|
||||
for _, i := range catalogue[page] {
|
||||
if i.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/* ── Query handling ─────────────────────────────────────────────────────── */
|
||||
|
||||
func TestQueryNormalization(t *testing.T) {
|
||||
want := intents(ask("positions", "pipeline"))
|
||||
if len(want) == 0 {
|
||||
t.Fatal("the baseline query matched nothing")
|
||||
}
|
||||
|
||||
// Every one of these is the same question typed differently: case,
|
||||
// surrounding whitespace, punctuation and control characters carry no
|
||||
// meaning, so all of them must rank identically.
|
||||
for _, query := range []string{
|
||||
" pipeline ", "PIPELINE", "PiPeLiNe", "\tpipeline\n",
|
||||
"pipeline?", "\"pipeline\"", "pipeline!!!", "…pipeline…",
|
||||
"(pipeline)", "**pipeline**", "pipeline\x00\x01", "\u200bpipeline",
|
||||
} {
|
||||
if got := intents(ask("positions", query)); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("query %q: got %v, want %v", query, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hostile input is data like any other. There is no query to inject into — the
|
||||
// normalized text is compared against a fixed table of literals and never
|
||||
// reaches SQL, a template or a shell — so the property under test is that such
|
||||
// a query is ranked rather than refused, and that it can only ever produce
|
||||
// entries this page declares.
|
||||
func TestHostileInputIsJustText(t *testing.T) {
|
||||
for _, query := range []string{
|
||||
"pipeline'; DROP TABLE job_postings; --",
|
||||
"pipeline\" OR 1=1 --",
|
||||
"<script>alert('pipeline')</script>",
|
||||
"{{7*7}} pipeline ${jndi:ldap://x/y}",
|
||||
"../../etc/passwd pipeline",
|
||||
"pipeline%00%0d%0aSet-Cookie:+x=1",
|
||||
strings.Repeat("' OR ''='", 40),
|
||||
} {
|
||||
for _, s := range ask("positions", query) {
|
||||
if !declaredOn("positions", s.Intent) {
|
||||
t.Errorf("query %q produced %q, which positions does not declare", query, s.Intent)
|
||||
}
|
||||
if !declaredText("positions", s) {
|
||||
t.Errorf("query %q produced unrecognised text %q", query, s.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// declaredText reports whether a suggestion's wording came from the catalogue —
|
||||
// either an intent's own Text, or its subject phrased in a shape it declares.
|
||||
// Nothing the caller typed may appear in a response.
|
||||
func declaredText(page string, got Suggestion) bool {
|
||||
for _, i := range catalogue[page] {
|
||||
if i.ID != got.Intent {
|
||||
continue
|
||||
}
|
||||
if got.Capability == "" {
|
||||
return got.Text == i.Text
|
||||
}
|
||||
for _, s := range shapes {
|
||||
if s.id == got.Capability {
|
||||
return got.Text == i.shaped(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fewer than two meaningful characters is not a question yet. Punctuation and
|
||||
// whitespace are not meaningful.
|
||||
func TestShortQueriesAreEmpty(t *testing.T) {
|
||||
for _, query := range []string{"", " ", "\n\t ", "p", " p ", "?", "!!!", "-", "€", " , "} {
|
||||
if got := ask("positions", query); len(got) != 0 {
|
||||
t.Errorf("query %q: got %v, want none", query, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The panel calls this on every keystroke, so a half-typed word has to match.
|
||||
func TestPrefixMatchingWhileTyping(t *testing.T) {
|
||||
full := intents(ask("positions", "pipeline"))
|
||||
for _, query := range []string{"pip", "pipe", "pipel", "pipelin", "pipeline", "pipelines"} {
|
||||
got := intents(ask("positions", query))
|
||||
if len(got) == 0 {
|
||||
t.Fatalf("query %q matched nothing; the panel would blank mid-word", query)
|
||||
}
|
||||
if query != "pipelines" && !reflect.DeepEqual(got, full) {
|
||||
t.Errorf("query %q: got %v, want %v", query, got, full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A long paste ranks on its opening words rather than being refused.
|
||||
func TestOverlongQueryIsTruncatedNotRejected(t *testing.T) {
|
||||
query := "pipeline " + strings.Repeat("x", 5000)
|
||||
got := intents(ask("positions", query))
|
||||
if len(got) == 0 {
|
||||
t.Fatal("an overlong query was refused instead of truncated")
|
||||
}
|
||||
if !reflect.DeepEqual(got, intents(ask("positions", "pipeline"))) {
|
||||
t.Fatalf("an overlong query ranked differently: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Shapes ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
// Asking for a section type names it in the answer and phrases the suggestion
|
||||
// in those terms — the brief's "Show hiring activity as a flow".
|
||||
func TestShapedSuggestions(t *testing.T) {
|
||||
got := ask("positions", "show hiring activity as a flow")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d suggestions, want 1: %+v", len(got), got)
|
||||
}
|
||||
want := Suggestion{
|
||||
Text: "Show hiring activity as a flow",
|
||||
Intent: "hiring-operations",
|
||||
Capability: "flow",
|
||||
}
|
||||
if got[0] != want {
|
||||
t.Fatalf("got %+v, want %+v", got[0], want)
|
||||
}
|
||||
|
||||
summarized := ask("positions", "summarize hiring activity")
|
||||
if len(summarized) != 1 || summarized[0].Capability != "summary" ||
|
||||
summarized[0].Text != "Summarize hiring activity" {
|
||||
t.Fatalf("got %+v", summarized)
|
||||
}
|
||||
}
|
||||
|
||||
// A shape alone is a question about the page. A shape after a subject is a
|
||||
// question about that subject, and the other readings that merely support the
|
||||
// shape are padding — which this endpoint does not do.
|
||||
func TestShapeAloneAnswersThePageButNeverPads(t *testing.T) {
|
||||
alone := ask("control-center", "summarize")
|
||||
if len(alone) != MaxSuggestions {
|
||||
t.Fatalf("a bare shape returned %d suggestions, want %d: %+v",
|
||||
len(alone), MaxSuggestions, alone)
|
||||
}
|
||||
for _, s := range alone {
|
||||
if s.Capability != "summary" {
|
||||
t.Fatalf("got capability %q, want summary: %+v", s.Capability, s)
|
||||
}
|
||||
}
|
||||
|
||||
// "attention" names one reading; nothing else may ride along on the shape.
|
||||
withSubject := ask("control-center", "summarize what needs attention")
|
||||
if len(withSubject) != 1 || withSubject[0].Intent != "attention-required" {
|
||||
t.Fatalf("got %+v, want only attention-required", withSubject)
|
||||
}
|
||||
}
|
||||
|
||||
// A shape an intent cannot be drawn as leaves its wording alone.
|
||||
func TestUnsupportedShapeIsNotClaimed(t *testing.T) {
|
||||
for _, s := range ask("create-position", "adjust the weights") {
|
||||
if s.Capability == "weights" {
|
||||
t.Fatalf("offered a weights rendering nothing declares: %+v", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Response limits ────────────────────────────────────────────────────── */
|
||||
|
||||
func TestNeverMoreThanThreeAndNeverDuplicated(t *testing.T) {
|
||||
// A query broad enough to match everything the page has.
|
||||
queries := []string{
|
||||
"position pipeline attention risk draft hiring activity waiting candidates",
|
||||
"candidate score interview decision risk pipeline screening",
|
||||
"summarize", "attention risk", "who what how many",
|
||||
}
|
||||
|
||||
for _, page := range Pages() {
|
||||
for _, query := range queries {
|
||||
got := Suggest(page, query, domain.RoleAdmin)
|
||||
if len(got) > MaxSuggestions {
|
||||
t.Fatalf("page %q query %q: %d suggestions, cap is %d",
|
||||
page, query, len(got), MaxSuggestions)
|
||||
}
|
||||
|
||||
seenIntent, seenText := map[string]bool{}, map[string]bool{}
|
||||
for _, s := range got {
|
||||
if s.Text == "" || s.Intent == "" {
|
||||
t.Fatalf("page %q query %q: incomplete suggestion %+v", page, query, s)
|
||||
}
|
||||
if seenIntent[s.Intent] {
|
||||
t.Fatalf("page %q query %q: duplicate intent %q", page, query, s.Intent)
|
||||
}
|
||||
if seenText[strings.ToLower(s.Text)] {
|
||||
t.Fatalf("page %q query %q: duplicate text %q", page, query, s.Text)
|
||||
}
|
||||
seenIntent[s.Intent], seenText[strings.ToLower(s.Text)] = true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The same request must answer the same way every time — the panel re-issues it
|
||||
// on every keystroke, and a list that reshuffles under the cursor is unusable.
|
||||
func TestSuggestIsDeterministic(t *testing.T) {
|
||||
for _, page := range Pages() {
|
||||
first := Suggest(page, "attention risk pipeline summary", domain.RoleAdmin)
|
||||
for i := 0; i < 20; i++ {
|
||||
again := Suggest(page, "attention risk pipeline summary", domain.RoleAdmin)
|
||||
if !reflect.DeepEqual(first, again) {
|
||||
t.Fatalf("page %q: run %d differed\n first: %+v\n again: %+v",
|
||||
page, i, first, again)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty, never nil: `{"suggestions": []}` and not `{"suggestions": null}`.
|
||||
func TestNoMatchIsAnEmptySliceNotNil(t *testing.T) {
|
||||
for _, c := range []struct{ page, query string }{
|
||||
{"positions", "sourdough"}, {"positions", ""}, {"nowhere", "pipeline"},
|
||||
} {
|
||||
got := ask(c.page, c.query)
|
||||
if got == nil {
|
||||
t.Fatalf("page %q query %q: got nil, want an empty slice", c.page, c.query)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("page %q query %q: got %v", c.page, c.query, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Authorization ──────────────────────────────────────────────────────── */
|
||||
|
||||
// Talent may list job applications, but only their own — so a reading across
|
||||
// the organization's pipeline is not theirs to be offered, even though the
|
||||
// operation itself is permitted. The same holds for postings, profiles, staff,
|
||||
// evidence and the audit log.
|
||||
//
|
||||
// The exception is stated rather than hidden: `courses` is the one resource in
|
||||
// the policy table that talent lists unscoped, because the training library is
|
||||
// shared platform-wide and everybody learns from it. So the two Forge readings
|
||||
// that ask only what the library holds survive, and every other Forge reading —
|
||||
// evaluation, workforce usage, gaps, all of which read evidence or profiles —
|
||||
// does not. If that ever widens, this test says exactly what widened.
|
||||
func TestTalentIsOfferedOnlyUnscopedReadings(t *testing.T) {
|
||||
pages := []string{
|
||||
"control-center", "positions", "candidates", "candidates-analysis",
|
||||
"analytics", "activity", "talent-pool", "hired-history", "krow-forge",
|
||||
"create-position",
|
||||
}
|
||||
queries := []string{
|
||||
"pipeline", "attention", "candidate", "position", "risk", "summarize",
|
||||
"hiring", "score", "activity", "who", "how many", "library", "skill",
|
||||
"published", "gaps", "evaluation", "weights", "credential",
|
||||
}
|
||||
|
||||
allowed := map[string]bool{"krow-forge/forge-library": true, "krow-forge/forge-published": true}
|
||||
|
||||
for _, page := range pages {
|
||||
for _, query := range queries {
|
||||
for _, s := range Suggest(page, query, domain.RoleTalent) {
|
||||
if !allowed[page+"/"+s.Intent] {
|
||||
t.Errorf("talent was offered %q on %q for %q", s.Intent, page, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And the operator's own Forge readings stay the operator's.
|
||||
for _, id := range []string{"forge-evaluation", "forge-workforce", "forge-gaps"} {
|
||||
for _, s := range Suggest("krow-forge", "evaluation workforce gaps", domain.RoleTalent) {
|
||||
if s.Intent == id {
|
||||
t.Errorf("talent was offered the operator reading %q", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(Suggest("krow-forge", "evaluation workforce gaps", domain.RoleAdmin)) == 0 {
|
||||
t.Error("admin was offered none of them either; the query no longer matches")
|
||||
}
|
||||
}
|
||||
|
||||
// The filter is not a blanket refusal: what a talent caller may genuinely ask —
|
||||
// about their own account — is still offered. Otherwise the test above would
|
||||
// pass with the role check stubbed out to "deny".
|
||||
func TestTalentIsStillOfferedTheirOwnReadings(t *testing.T) {
|
||||
for _, query := range []string{"permission", "password", "my recent activity"} {
|
||||
if got := Suggest("profile", query, domain.RoleTalent); len(got) == 0 {
|
||||
t.Fatalf("talent was offered nothing on profile for %q", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A role the API does not recognise authorizes nothing, matching policy.go.
|
||||
func TestUnknownRoleIsOfferedNothing(t *testing.T) {
|
||||
for _, role := range []domain.Role{"", "root", "superuser", "Admin"} {
|
||||
for _, page := range Pages() {
|
||||
if got := Suggest(page, "attention pipeline permission", role); len(got) != 0 {
|
||||
t.Fatalf("role %q was offered %v on %q", role, intents(got), page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every permission decision must come from the policy table, not from a list
|
||||
// kept here. This asserts the mechanism rather than a particular outcome: an
|
||||
// intent is offered exactly when policy.go allows every reading it declares.
|
||||
func TestPermissionsComeFromThePolicyTable(t *testing.T) {
|
||||
for _, role := range []domain.Role{domain.RoleAdmin, domain.RoleEmployer, domain.RoleTalent} {
|
||||
for page, list := range catalogue {
|
||||
for _, intent := range list {
|
||||
want := true
|
||||
for _, need := range intent.Reads {
|
||||
res, ok := domain.ResourceByPath[need.Resource]
|
||||
if !ok || !res.Supports(need.Op) || !res.Policy.Allows(need.Op, role) {
|
||||
want = false
|
||||
break
|
||||
}
|
||||
if need.OrgWide && res.Policy.ScopeFor(role).Kind != domain.ScopeNone {
|
||||
want = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if got := intent.permitted(role); got != want {
|
||||
t.Errorf("%s/%s for %s: permitted=%v, policy says %v",
|
||||
page, intent.ID, role, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── The catalogue itself ───────────────────────────────────────────────── */
|
||||
|
||||
func TestCatalogueIsWellFormed(t *testing.T) {
|
||||
for page, list := range catalogue {
|
||||
if definition.CanonicalPage(page) != page {
|
||||
t.Errorf("page key %q is not a canonical surface", page)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
t.Errorf("page %q has no intents; omit the key instead", page)
|
||||
}
|
||||
|
||||
ids, texts := map[string]bool{}, map[string]bool{}
|
||||
for _, intent := range list {
|
||||
where := fmt.Sprintf("%s/%s", page, intent.ID)
|
||||
|
||||
if intent.ID == "" || intent.Text == "" {
|
||||
t.Errorf("%s: an intent needs both an id and a text", where)
|
||||
}
|
||||
if ids[intent.ID] {
|
||||
t.Errorf("%s: duplicate intent id on this page", where)
|
||||
}
|
||||
if texts[strings.ToLower(intent.Text)] {
|
||||
t.Errorf("%s: duplicate suggestion text on this page", where)
|
||||
}
|
||||
ids[intent.ID], texts[strings.ToLower(intent.Text)] = true, true
|
||||
|
||||
if len(intent.Terms) == 0 {
|
||||
t.Errorf("%s: no terms, so it can never be suggested", where)
|
||||
}
|
||||
for _, term := range intent.Terms {
|
||||
if term != strings.ToLower(strings.TrimSpace(term)) || term == "" {
|
||||
t.Errorf("%s: term %q must be lower case and trimmed", where, term)
|
||||
}
|
||||
if _, _, meaningful := normalize(term); meaningful < MinQueryChars {
|
||||
t.Errorf("%s: term %q is shorter than the shortest query", where, term)
|
||||
}
|
||||
}
|
||||
|
||||
if len(intent.Shapes) > 0 && intent.Subject == "" {
|
||||
t.Errorf("%s: declares shapes but no subject to phrase them with", where)
|
||||
}
|
||||
for _, id := range intent.Shapes {
|
||||
if id == "summary" {
|
||||
t.Errorf("%s: `summary` applies to every subject and is never declared", where)
|
||||
}
|
||||
if !knownShape(id) {
|
||||
t.Errorf("%s: shape %q is not in the Owliver vocabulary", where, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, need := range intent.Reads {
|
||||
res, ok := domain.ResourceByPath[need.Resource]
|
||||
if !ok {
|
||||
t.Errorf("%s: reads %q, which is not a resource", where, need.Resource)
|
||||
continue
|
||||
}
|
||||
if !res.Supports(need.Op) {
|
||||
t.Errorf("%s: reads %q with an operation it does not serve", where, need.Resource)
|
||||
}
|
||||
// An intent nobody can be offered is dead weight, and usually a
|
||||
// typo in the resource path rather than a deliberate lockout.
|
||||
if !res.Policy.Allows(need.Op, domain.RoleAdmin) {
|
||||
t.Errorf("%s: reads %q, which not even admin may list", where, need.Resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every id in the catalogue must be a capability the frontend actually
|
||||
// declares, because the id in a response is what the panel dispatches on. The
|
||||
// list is the union of the manifests in
|
||||
// src/components/ai-assistant/capabilities/, transcribed alongside the
|
||||
// catalogue; an id here that is absent there would be a suggestion the panel
|
||||
// cannot run.
|
||||
func TestIntentIDsAreFrontendCapabilities(t *testing.T) {
|
||||
frontend := map[string]bool{}
|
||||
for _, id := range []string{
|
||||
// CONTROL_CENTER_CAPABILITIES
|
||||
"platform-health", "workforce-summary", "hiring-operations", "pipeline-health",
|
||||
"attention-required", "recommendations",
|
||||
// POSITIONS_CAPABILITIES
|
||||
"position-drafts", "position-strength", "positions-attention", "hiring-priority",
|
||||
"candidates-waiting",
|
||||
// CANDIDATE_LIST_CAPABILITIES
|
||||
"candidates-attention", "top-candidates", "interview-ready", "screening-gaps",
|
||||
"pipeline-summary", "candidate-risk",
|
||||
// ADMIN_CANDIDATE_CAPABILITIES
|
||||
"recruitment-insights", "hiring-recommendations",
|
||||
// ADMIN_ANALYTICS_CAPABILITIES
|
||||
"hiring-trend", "department-performance", "position-conversion",
|
||||
// ACTIVITY_CAPABILITIES
|
||||
"audit-summary", "user-activity", "unusual-activity", "security-insights",
|
||||
// TALENT_POOL_CAPABILITIES
|
||||
"talent-priorities", "talent-summary", "talent-verification", "talent-availability",
|
||||
// HIRED_HISTORY_CAPABILITIES
|
||||
"hiring-outcomes", "hiring-strongest", "hiring-patterns", "hiring-recent",
|
||||
// FORGE_CAPABILITIES
|
||||
"forge-library", "forge-published", "forge-evaluation", "forge-workforce", "forge-gaps",
|
||||
// CREATE_POSITION_CAPABILITIES
|
||||
"vetting-weights", "position-benchmarks", "position-requirements", "position-spec-steps",
|
||||
// PROFILE_CAPABILITIES
|
||||
"profile-permissions", "profile-identity", "profile-security", "profile-preferences",
|
||||
"profile-activity", "profile-actions",
|
||||
} {
|
||||
frontend[id] = true
|
||||
}
|
||||
|
||||
used := map[string]bool{}
|
||||
for page, list := range catalogue {
|
||||
for _, intent := range list {
|
||||
used[intent.ID] = true
|
||||
if !frontend[intent.ID] {
|
||||
t.Errorf("%s/%s names no frontend capability", page, intent.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
for id := range frontend {
|
||||
if !used[id] {
|
||||
t.Errorf("capability %q is declared here but suggested on no page", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func knownShape(id string) bool {
|
||||
for _, s := range shapes {
|
||||
if s.id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The shape vocabulary is closed and mirrors OWLIVER_CAPABILITIES.
|
||||
func TestShapeVocabularyMatchesTheFrontend(t *testing.T) {
|
||||
want := []string{"summary", "flow", "stats", "list", "table", "timeline",
|
||||
"progress", "weights", "insight", "card"}
|
||||
|
||||
got := make([]string, len(shapes))
|
||||
for i, s := range shapes {
|
||||
got[i] = s.id
|
||||
if len(s.terms) == 0 {
|
||||
t.Errorf("shape %q has no terms", s.id)
|
||||
}
|
||||
if s.id != "summary" && s.phrase == "" {
|
||||
t.Errorf("shape %q has no phrase to read inside a sentence", s.id)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("shapes are %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing internal may reach a response: no terms, no resource names, no
|
||||
// scores. The struct is the whole contract, so this asserts its shape.
|
||||
func TestSuggestionExposesNothingInternal(t *testing.T) {
|
||||
fields := reflect.VisibleFields(reflect.TypeOf(Suggestion{}))
|
||||
if len(fields) != 3 {
|
||||
t.Fatalf("Suggestion has %d fields; the response contract is text, intent, capability", len(fields))
|
||||
}
|
||||
want := map[string]string{
|
||||
"Text": `json:"text"`,
|
||||
"Intent": `json:"intent"`,
|
||||
"Capability": `json:"capability,omitempty"`,
|
||||
}
|
||||
for _, f := range fields {
|
||||
if string(f.Tag) != want[f.Name] {
|
||||
t.Errorf("field %s has tag %q, want %q", f.Name, f.Tag, want[f.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user