diff --git a/modules/brother/README.md b/modules/brother/README.md index 44733dba..b4a3902a 100644 --- a/modules/brother/README.md +++ b/modules/brother/README.md @@ -41,9 +41,9 @@ makes it (mode 0755) when it files the first scan. | tool | does | |---|---| -| `scan_now` | scans, crops, files and emits; answers the event's body | -| `scan_status` | whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is `answers: false` with why. This is how its health is read: the module has no long-running resource to declare a health check on | -| `scan_list` | the newest filed scans, newest first (`limit`, default 20). Read-only | +| `scanner.scan` | scans, crops, files and emits; answers the event's body | +| `scanner.status` | whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is `answers: false` with why. This is how its health is read: the module has no long-running resource to declare a health check on | +| `scanner.list` | the newest filed scans, newest first (`limit`, default 20). Read-only | ## The event @@ -58,7 +58,7 @@ Each directory or archive this module declares says how it treats a file it did - `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 ยง2). - The scan folder (`scan-folder`) is declared by no manifest. The module writes only the `scan-*.pdf` files it files and their `.scan-*.tmp` while they are written; it never changes, moves or removes any other file there, - and `scan_list` only reads. + and `scanner.list` only reads. ## Tests diff --git a/modules/brother/cmd/brother/main.go b/modules/brother/cmd/brother/main.go index 9d699f48..11866ab7 100644 --- a/modules/brother/cmd/brother/main.go +++ b/modules/brother/cmd/brother/main.go @@ -21,7 +21,7 @@ func emit(body map[string]any, id string) error { return stdio.Emit(EventAdded, body, stdio.EmitOptions{MessageID: id}) } -// tools are the three tools. The settings are read at each call, so a changed config value is the next +// tools are the scanner seat's three verbs, served as . (ADR 0467 clause 2). The settings are read at each call, so a changed config value is the next // call's. func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool { service := func() (*Service, error) { @@ -33,7 +33,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool } return []stdio.Tool{ { - Name: "scan_now", + Name: "scanner.scan", Description: "Scan the glass of the configured network scanner, cut the document out of the platen's white, " + "file it as a one-page PDF in the scan folder and say scan.added on the bus. Answers the event's body: " + "the file's path, its size and sha256, its real size in millimetres, the resolution the scanner " + @@ -47,7 +47,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool }, }, { - Name: "scan_status", + Name: "scanner.status", Description: "Whether the configured scanner answers, and what it is doing: its state (Idle or Processing) " + "and its document feeder's state. A scanner that does not answer is said as answers false with why. Read-only.", Run: func(map[string]any) (any, error) { @@ -61,7 +61,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool }, }, { - Name: "scan_list", + Name: "scanner.list", Description: "The newest scans filed in the scan folder, newest first, with their size and time. Read-only.", Input: map[string]any{ "type": "object", diff --git a/modules/brother/cmd/brother/manifest_test.go b/modules/brother/cmd/brother/manifest_test.go index 86c60a74..ea3daea0 100644 --- a/modules/brother/cmd/brother/manifest_test.go +++ b/modules/brother/cmd/brother/manifest_test.go @@ -11,6 +11,8 @@ import ( type manifest struct { Emits []string `json:"emits"` + Seats []seat `json:"seats"` + Claims []seat `json:"claims"` Tools []string `json:"tools"` Config map[string]map[string]any `json:"config"` Resources []map[string]any `json:"resources"` @@ -19,6 +21,12 @@ type manifest struct { } `json:"build"` } +type seat struct { + Name string `json:"name"` + Scope string `json:"scope"` + Serves []string `json:"serves"` +} + func readManifest(t *testing.T) manifest { t.Helper() raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) @@ -116,3 +124,41 @@ func TestTheConfigReachesTheCodeThroughItsSettingsFile(t *testing.T) { } } } + +// 1453/AC6: the manifest declares the scanner seat, held once for the mesh, and claims it, with the verbs +// ADR 0467 clause 2 names; the module serves each verb as the tool .. +func TestTheManifestDeclaresAndClaimsTheScannerSeat(t *testing.T) { + m := readManifest(t) + want := seat{Name: "scanner", Scope: "mesh", Serves: []string{"scan", "status", "list"}} + for what, got := range map[string][]seat{"seats": m.Seats, "claims": m.Claims} { + found := false + for _, s := range got { + if s.Name == "scanner" { + found = true + if !reflect.DeepEqual(s, want) { + t.Errorf("%s: scanner = %+v, want %+v", what, s, want) + } + } + } + if !found { + t.Errorf("%s does not hold the scanner seat", what) + } + } +} + +// 1453/AC6 (issue 1446): the three preferences are not trusted, so a person's own setting is not refused. +func TestThePreferencesAreNotTrusted(t *testing.T) { + m := readManifest(t) + for _, k := range []string{"resolution", "color-mode", "crop"} { + if v, ok := m.Config[k]["trusted"]; !ok || v != false { + t.Errorf("%s: trusted = %v, want false", k, v) + } + } +} + +// 1453/AC6: the scan module is gone from the catalogue. +func TestTheScanModuleIsGone(t *testing.T) { + if _, err := os.Stat(filepath.Join("..", "..", "..", "scan")); !os.IsNotExist(err) { + t.Errorf("modules/scan still exists (err %v)", err) + } +} diff --git a/modules/brother/cmd/brother/service_test.go b/modules/brother/cmd/brother/service_test.go index 845b892a..94b7f43c 100644 --- a/modules/brother/cmd/brother/service_test.go +++ b/modules/brother/cmd/brother/service_test.go @@ -209,7 +209,7 @@ func TestEveryToolSaysWhichValueIsUnsetAndWritesNothing(t *testing.T) { js := strings.ReplaceAll(strings.Replace(c.json, "%s", f.base(), 1), "FOLDER", folder) var out []said tl := tools(settingsFile(t, js), func(b map[string]any, id string) error { out = append(out, said{b, id, true}); return nil }, time.Now) - for _, name := range []string{"scan_now", "scan_status", "scan_list"} { + for _, name := range []string{"scanner.scan", "scanner.status", "scanner.list"} { _, err := tool(t, tl, name).Run(map[string]any{}) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s with %s: error = %v, want it to name %q", name, c.json, err, c.want) @@ -227,7 +227,7 @@ func TestEveryToolSaysWhichValueIsUnsetAndWritesNothing(t *testing.T) { // 1440/AC7: the settings file is not there at all. func TestAMissingSettingsFileIsSaid(t *testing.T) { tl := tools(filepath.Join(t.TempDir(), "none.json"), nil, time.Now) - if _, err := tool(t, tl, "scan_status").Run(nil); err == nil || !strings.Contains(err.Error(), "settings") { + if _, err := tool(t, tl, "scanner.status").Run(nil); err == nil || !strings.Contains(err.Error(), "settings") { t.Fatalf("error = %v", err) } } @@ -260,7 +260,7 @@ func TestScanListToolListsTheNewestFirst(t *testing.T) { os.Chtimes(p, base.Add(time.Duration(i)*time.Hour), base.Add(time.Duration(i)*time.Hour)) } tl := tools(settingsFile(t, `{"scanner":"http://192.0.2.10/eSCL","scan-folder":"`+folder+`"}`), nil, time.Now) - got, err := tool(t, tl, "scan_list").Run(map[string]any{"limit": float64(5)}) + got, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(5)}) if err != nil { t.Fatal(err) } @@ -268,7 +268,7 @@ func TestScanListToolListsTheNewestFirst(t *testing.T) { if len(scans) != 2 || filepath.Base(scans[0].Path) != "scan-2.pdf" { t.Fatalf("scans = %v", scans) } - if _, err := tool(t, tl, "scan_list").Run(map[string]any{"limit": float64(0)}); err == nil { + if _, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(0)}); err == nil { t.Error("limit 0 accepted") } } @@ -280,7 +280,7 @@ func TestScanNowToolScansWithTheFileSettings(t *testing.T) { var out []said tl := tools(settingsFile(t, `{"scanner":"`+f.base()+`","scan-folder":"`+folder+`","resolution":300,"color-mode":"color","crop":true}`), func(b map[string]any, id string) error { out = append(out, said{b, id, true}); return nil }, time.Now) - got, err := tool(t, tl, "scan_now").Run(map[string]any{}) + got, err := tool(t, tl, "scanner.scan").Run(map[string]any{}) if err != nil { t.Fatal(err) } diff --git a/modules/brother/module.json b/modules/brother/module.json index ce4c1a00..98add799 100644 --- a/modules/brother/module.json +++ b/modules/brother/module.json @@ -28,6 +28,7 @@ 300, 600 ], + "trusted": false, "default": 300 }, "color-mode": { @@ -37,18 +38,42 @@ "color", "gray" ], + "trusted": false, "default": "color" }, "crop": { "type": "boolean", "description": "Whether the document is cut out of the platen's white lid; false files the whole platen.", + "trusted": false, "default": true } }, + "seats": [ + { + "name": "scanner", + "scope": "mesh", + "serves": [ + "scan", + "status", + "list" + ] + } + ], + "claims": [ + { + "name": "scanner", + "scope": "mesh", + "serves": [ + "scan", + "status", + "list" + ] + } + ], "tools": [ - "scan_now", - "scan_status", - "scan_list" + "scanner.scan", + "scanner.status", + "scanner.list" ], "resources": [ {