|
@@ -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"},
|