Jelajahi Sumber

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

fszontagh 1 bulan lalu
induk
melakukan
17e5107774
1 mengubah file dengan 36 tambahan dan 15 penghapusan
  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:**
 **Interfaces:**
 - Produces: two new optional top-level keys in a case file.
 - 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.
   - `"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.
   - `"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**
 - [ ] **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))
     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:
 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.
 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:
 Run:
 ```bash
 ```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.
 Expected: no `FAILED:` lines. Every existing case lacks both new keys, so all take the unchanged path.
 
 
-- [ ] **Step 5: Commit**
+- [ ] **Step 6: Commit**
 
 
 ```bash
 ```bash
 git add scripts/verify-node.py
 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:**
 **Interfaces:**
 - Consumes: `smartbotic.fs.readdir(path)` from Task 2.
 - 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**
 - [ ] **Step 1: Write the node**
 
 
@@ -395,7 +406,7 @@ const configSchema = {
             type: 'string',
             type: 'string',
             title: 'Cursor Collection',
             title: 'Cursor Collection',
             description: 'Collection holding the last-seen state',
             description: 'Collection holding the last-seen state',
-            default: '_watch_cursors'
+            default: 'watch_cursors'
         }
         }
     },
     },
     required: ['directory']
     required: ['directory']
@@ -423,7 +434,7 @@ async function execute(config, input, context) {
         throw new Error('File Watch: a directory is required');
         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 workflowId = (context && context.workflowId) || 'unknown';
     const nodeId = (context && context.nodeId) || 'file-watch';
     const nodeId = (context && context.nodeId) || 'file-watch';
     const cursorId = workflowId + ':' + nodeId;
     const cursorId = workflowId + ':' + nodeId;
@@ -510,10 +521,15 @@ If either is absent, the cursor id would collapse to `unknown:file-watch` and tw
 ```json
 ```json
 {
 {
   "name": "verify-file-watch",
   "name": "verify-file-watch",
+  "settings": {
+    "storagePermissions": {
+      "collections": { "watch_cursors": "read-write" }
+    }
+  },
   "nodes": [
   "nodes": [
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "setup", "name": "Setup", "type": "code", "position": {"x": 0, "y": 100},
     {"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},
     {"id": "first", "name": "First Run", "type": "file-watch", "position": {"x": 0, "y": 200},
      "config": {"directory": "/tmp/sb-file-watch-test", "emitOnFirstRun": false}},
      "config": {"directory": "/tmp/sb-file-watch-test", "emitOnFirstRun": false}},
     {"id": "add", "name": "Add A File", "type": "code", "position": {"x": 0, "y": 300},
     {"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`
 - Create: `tests/nodes/database-change.json`
 
 
 **Interfaces:**
 **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**
 - [ ] **Step 1: Write the node**
 
 
@@ -608,7 +624,7 @@ const configSchema = {
         cursorCollection: {
         cursorCollection: {
             type: 'string',
             type: 'string',
             title: 'Cursor Collection',
             title: 'Cursor Collection',
-            default: '_watch_cursors'
+            default: 'watch_cursors'
         }
         }
     },
     },
     required: ['collection']
     required: ['collection']
@@ -638,7 +654,7 @@ async function execute(config, input, context) {
     }
     }
 
 
     const timestampField = config.timestampField || '_updatedAt';
     const timestampField = config.timestampField || '_updatedAt';
-    const cursorCollection = config.cursorCollection || '_watch_cursors';
+    const cursorCollection = config.cursorCollection || 'watch_cursors';
     const maxDocuments = Number(config.maxDocuments) || 100;
     const maxDocuments = Number(config.maxDocuments) || 100;
     const workflowId = (context && context.workflowId) || 'unknown';
     const workflowId = (context && context.workflowId) || 'unknown';
     const nodeId = (context && context.nodeId) || 'database-change';
     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
 ```json
 {
 {
   "name": "verify-database-change",
   "name": "verify-database-change",
+  "settings": {
+    "storagePermissions": {
+      "collections": { "watch_cursors": "read-write", "dbchange_test": "read-write" }
+    }
+  },
   "nodes": [
   "nodes": [
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
     {"id": "setup", "name": "Setup", "type": "code", "position": {"x": 0, "y": 100},
     {"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},
     {"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},
     {"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},
     {"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},
     {"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": [
   "connections": [
     {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "setup", "targetInput": "data"},
     {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "setup", "targetInput": "data"},