Просмотр исходного кода

docs: fixtures declare storage permissions, and cursors avoid underscore collections

fszontagh 1 месяц назад
Родитель
Сommit
17e5107774
1 измененных файлов с 36 добавлено и 15 удалено
  1. 36 15
      docs/superpowers/plans/2026-08-05-tier-2-triggers.md

+ 36 - 15
docs/superpowers/plans/2026-08-05-tier-2-triggers.md

@@ -54,6 +54,11 @@ Three later tasks need assertions the harness cannot express: a webhook's status
 **Interfaces:**
 - Produces: two new optional top-level keys in a case file.
   - `"http"`: `{method, path, body, expectStatus, expectHeaders, expectBodyContains}` - instead of executing the workflow through `/execute`, the harness calls `path` on the webserver directly and asserts on the response. Used for webhook cases.
+  - `"settings"`: passed through as the created workflow's `settings` object
+    instead of the hardcoded `{}`. Without it a fixture cannot grant
+    `storagePermissions`, and every `smartbotic.storage.*` call a node makes
+    fails with "No write access to collection". No existing fixture uses
+    storage, which is why this went unnoticed.
   - `"resume"`: `{waitForStatus, tokenFrom, payload}` - after starting the execution, poll until the execution reaches `waitForStatus`, read the token from the node output named by `tokenFrom`, POST it to the resume endpoint with `payload`, then continue polling to completion before running the normal `expect` assertions.
 
 - [ ] **Step 1: Add the HTTP case mode**
@@ -132,7 +137,13 @@ def run_resume_case(case, token, execution_id):
     print("resumed %s with token %s" % (execution_id, resume_token))
 ```
 
-- [ ] **Step 3: Wire both into main**
+- [ ] **Step 3: Pass a fixture's settings through**
+
+In `main`, the workflow is created with `"settings": {}` hardcoded. Replace that
+with `case.get("settings", {})`, so a fixture can declare what its nodes need.
+Every existing fixture omits the key and gets `{}` exactly as before.
+
+- [ ] **Step 4: Wire both into main**
 
 In `main`, immediately after the workflow is created inside the `try` block, add the HTTP branch before the existing execute call:
 
@@ -154,7 +165,7 @@ Then, after `execution_id` is obtained and before the existing polling loop, add
 
 The existing polling loop then runs unchanged and sees the resumed execution through to completion.
 
-- [ ] **Step 4: Confirm nothing regressed**
+- [ ] **Step 5: Confirm nothing regressed**
 
 Run:
 ```bash
@@ -162,7 +173,7 @@ for f in tests/nodes/*.json; do python3 scripts/verify-node.py "$f" >/dev/null |
 ```
 Expected: no `FAILED:` lines. Every existing case lacks both new keys, so all take the unchanged path.
 
-- [ ] **Step 5: Commit**
+- [ ] **Step 6: Commit**
 
 ```bash
 git add scripts/verify-node.py
@@ -352,7 +363,7 @@ git commit -m "feat: fs.readdir, and a workflow id a node can see"
 
 **Interfaces:**
 - Consumes: `smartbotic.fs.readdir(path)` from Task 2.
-- Produces: output `{files: [...], count, isFirstRun}`. Cursor documents live in the `_watch_cursors` collection, keyed `<workflowId>:<nodeId>`.
+- Produces: output `{files: [...], count, isFirstRun}`. Cursor documents live in the `watch_cursors` collection, keyed `<workflowId>:<nodeId>`.
 
 - [ ] **Step 1: Write the node**
 
@@ -395,7 +406,7 @@ const configSchema = {
             type: 'string',
             title: 'Cursor Collection',
             description: 'Collection holding the last-seen state',
-            default: '_watch_cursors'
+            default: 'watch_cursors'
         }
     },
     required: ['directory']
@@ -423,7 +434,7 @@ async function execute(config, input, context) {
         throw new Error('File Watch: a directory is required');
     }
 
-    const collection = config.cursorCollection || '_watch_cursors';
+    const collection = config.cursorCollection || 'watch_cursors';
     const workflowId = (context && context.workflowId) || 'unknown';
     const nodeId = (context && context.nodeId) || 'file-watch';
     const cursorId = workflowId + ':' + nodeId;
@@ -510,10 +521,15 @@ If either is absent, the cursor id would collapse to `unknown:file-watch` and tw
 ```json
 {
   "name": "verify-file-watch",
+  "settings": {
+    "storagePermissions": {
+      "collections": { "watch_cursors": "read-write" }
+    }
+  },
   "nodes": [
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "setup", "name": "Setup", "type": "code", "position": {"x": 0, "y": 100},
-     "config": {"code": "const dir = '/tmp/sb-file-watch-test';\nconst listing = smartbotic.fs.readdir(dir);\nif (listing.success) {\n    for (const e of listing.entries) { smartbotic.fs.unlink(e.path); }\n}\nsmartbotic.fs.mkdir(dir);\nsmartbotic.fs.writeFile(dir + '/existing.txt', smartbotic.utils.base64Encode('old'));\nsmartbotic.storage.delete('_watch_cursors', 'verify-file-watch:first');\nsmartbotic.storage.delete('_watch_cursors', 'verify-file-watch:second');\nreturn { dir: dir };"}},
+     "config": {"code": "const dir = '/tmp/sb-file-watch-test';\nconst listing = smartbotic.fs.readdir(dir);\nif (listing.success) {\n    for (const e of listing.entries) { smartbotic.fs.unlink(e.path); }\n}\nsmartbotic.fs.mkdir(dir);\nsmartbotic.fs.writeFile(dir + '/existing.txt', smartbotic.utils.base64Encode('old'));\nsmartbotic.storage.delete('watch_cursors', 'verify-file-watch:first');\nsmartbotic.storage.delete('watch_cursors', 'verify-file-watch:second');\nreturn { dir: dir };"}},
     {"id": "first", "name": "First Run", "type": "file-watch", "position": {"x": 0, "y": 200},
      "config": {"directory": "/tmp/sb-file-watch-test", "emitOnFirstRun": false}},
     {"id": "add", "name": "Add A File", "type": "code", "position": {"x": 0, "y": 300},
@@ -559,7 +575,7 @@ git commit -m "feat: a File Watch trigger, reporting what changed in a directory
 - Create: `tests/nodes/database-change.json`
 
 **Interfaces:**
-- Produces: output `{documents: [...], count, isFirstRun}`. Cursor documents live in the same `_watch_cursors` collection, keyed `<workflowId>:<nodeId>`.
+- Produces: output `{documents: [...], count, isFirstRun}`. Cursor documents live in the same `watch_cursors` collection, keyed `<workflowId>:<nodeId>`.
 
 - [ ] **Step 1: Write the node**
 
@@ -608,7 +624,7 @@ const configSchema = {
         cursorCollection: {
             type: 'string',
             title: 'Cursor Collection',
-            default: '_watch_cursors'
+            default: 'watch_cursors'
         }
     },
     required: ['collection']
@@ -638,7 +654,7 @@ async function execute(config, input, context) {
     }
 
     const timestampField = config.timestampField || '_updatedAt';
-    const cursorCollection = config.cursorCollection || '_watch_cursors';
+    const cursorCollection = config.cursorCollection || 'watch_cursors';
     const maxDocuments = Number(config.maxDocuments) || 100;
     const workflowId = (context && context.workflowId) || 'unknown';
     const nodeId = (context && context.nodeId) || 'database-change';
@@ -734,18 +750,23 @@ Record the magnitude in your report. If those values are around 1.7e18 the norma
 ```json
 {
   "name": "verify-database-change",
+  "settings": {
+    "storagePermissions": {
+      "collections": { "watch_cursors": "read-write", "dbchange_test": "read-write" }
+    }
+  },
   "nodes": [
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "setup", "name": "Setup", "type": "code", "position": {"x": 0, "y": 100},
-     "config": {"code": "smartbotic.storage.delete('_watch_cursors', 'verify-database-change:first');\nsmartbotic.storage.delete('_watch_cursors', 'verify-database-change:second');\nsmartbotic.storage.insert('_dbchange_test', { name: 'before', at: Date.now() }, 'before-1');\nreturn { ready: true };"}},
+     "config": {"code": "smartbotic.storage.delete('watch_cursors', 'verify-database-change:first');\nsmartbotic.storage.delete('watch_cursors', 'verify-database-change:second');\nsmartbotic.storage.insert('dbchange_test', { name: 'before', at: Date.now() }, 'before-1');\nreturn { ready: true };"}},
     {"id": "first", "name": "First Run", "type": "database-change", "position": {"x": 0, "y": 200},
-     "config": {"collection": "_dbchange_test", "emitOnFirstRun": false}},
+     "config": {"collection": "dbchange_test", "emitOnFirstRun": false}},
     {"id": "add", "name": "Insert One", "type": "code", "position": {"x": 0, "y": 300},
-     "config": {"code": "smartbotic.utils.sleep(1100);\nsmartbotic.storage.insert('_dbchange_test', { name: 'after', at: Date.now() }, 'after-1');\nreturn { inserted: 'after-1' };"}},
+     "config": {"code": "smartbotic.utils.sleep(1100);\nsmartbotic.storage.insert('dbchange_test', { name: 'after', at: Date.now() }, 'after-1');\nreturn { inserted: 'after-1' };"}},
     {"id": "second", "name": "Second Run", "type": "database-change", "position": {"x": 0, "y": 400},
-     "config": {"collection": "_dbchange_test", "emitOnFirstRun": false}},
+     "config": {"collection": "dbchange_test", "emitOnFirstRun": false}},
     {"id": "cleanup", "name": "Cleanup", "type": "code", "position": {"x": 0, "y": 500},
-     "config": {"code": "smartbotic.storage.delete('_dbchange_test', 'before-1');\nsmartbotic.storage.delete('_dbchange_test', 'after-1');\nreturn { cleaned: true };"}}
+     "config": {"code": "smartbotic.storage.delete('dbchange_test', 'before-1');\nsmartbotic.storage.delete('dbchange_test', 'after-1');\nreturn { cleaned: true };"}}
   ],
   "connections": [
     {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "setup", "targetInput": "data"},