proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.
Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:
gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
gen_ts.py -> extension/src/shared/protocol/
gen_openrpc.py -> contracts/openrpc.json
gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.
59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.
run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.
core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# contracts/fixtures — golden request/response pairs
|
||||
|
||||
Every method has at least one success fixture. A method with no fixture is not done.
|
||||
|
||||
These files are replayed by `tests/conformance/` against **both** the generated C++ and a
|
||||
live server, which is what lets four lanes build in parallel and still be compatible: green
|
||||
fixtures mean the C++ daemon and the TypeScript extension agree, without either having ever
|
||||
run against the other. `tools/mockd` also answers from them, so the GUI and extension are
|
||||
developed against the same bytes conformance asserts.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
fixtures/
|
||||
├── *.json one success fixture per method
|
||||
├── errors/ error cases: auth, transport, not-found, bad path, timeout
|
||||
└── events/ one fixture per server-to-client notification
|
||||
```
|
||||
|
||||
## Shape
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "download.add — start an ISO now, into the Programs category",
|
||||
"description": "Why this case is worth pinning.",
|
||||
"transport": "uds", // optional: replay only on this transport
|
||||
"requires": "...", // optional: a condition a plain server cannot produce
|
||||
"kind": "timeout", // optional: the correct behaviour is *no reply*
|
||||
"request": { "jsonrpc": "2.0", "id": 11, "method": "download.add", "params": { } },
|
||||
"response": { "jsonrpc": "2.0", "id": 11, "result": { } },
|
||||
"assertions": [ "things a runner or a reviewer should check" ]
|
||||
}
|
||||
```
|
||||
|
||||
An event fixture carries `notification` instead of `request`/`response`.
|
||||
|
||||
`assertions` are prose, for the human writing the implementation. The runners check the
|
||||
machine-checkable parts: schema validity, error codes, shape, and the timeout.
|
||||
|
||||
## Placeholders
|
||||
|
||||
Some values cannot be pinned in a golden file. These stand in for them, and the runners
|
||||
treat them as "any value of the right shape":
|
||||
|
||||
| Placeholder | Means |
|
||||
|---|---|
|
||||
| `$uuid` | any UUID |
|
||||
| `$isoDate` | any RFC 3339 date-time |
|
||||
| `$opaque` | a credential-shaped string (a token) |
|
||||
| `$any` | any value |
|
||||
| `$taskId`, `$taskId2` | a task the runner creates during setup, and binds before replaying |
|
||||
|
||||
`$taskId` exists so a fixture never depends on a task id that only happens to exist in a
|
||||
seeded mock. The same fixture then runs against an empty `veloxd` and a populated `mockd`.
|
||||
|
||||
## Values are matched by shape, not by equality
|
||||
|
||||
A live daemon returns its own task ids and its own clock. Demanding byte-identical results
|
||||
would only teach the suite to lie, so the runners assert:
|
||||
|
||||
* the payload passes the **generated validator** — this is the real cross-language check;
|
||||
* the **key structure** matches the golden file, with no extra and no missing fields;
|
||||
* **error codes** match exactly.
|
||||
|
||||
A `null` where the golden shows a value is accepted: the validator has already ruled on
|
||||
whether null is legal there, and a golden file shows one plausible value, not the only one.
|
||||
|
||||
## `requires`: fixtures a mock cannot produce
|
||||
|
||||
Most error fixtures are *intrinsic* — a path outside the allowed roots, an out-of-range
|
||||
parameter, an unknown task id — and any correct server produces them from the request
|
||||
alone. Those are replayed everywhere.
|
||||
|
||||
Four are environmental: a 403 from an origin server, a full disk, a pairing lockout, a
|
||||
wedged daemon. They carry `requires`, are skipped by default, and are exercised where the
|
||||
condition can actually be arranged — `run.sh` starts a deliberately slow `mockd` to prove
|
||||
`capture.offer` fails open, and lane PKG/QA's `tools/testserver` covers the hostile-server
|
||||
cases in `tests/integration/`.
|
||||
|
||||
`errors/capture.offer.timeout.json` is the most important file in this directory. Its
|
||||
correct response is *no response*: past 750 ms the extension must abandon the offer and let
|
||||
Firefox download normally. A download manager that eats downloads when its daemon is down
|
||||
is worse than no download manager.
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "capture.getRules \u2014 the extension mirrors the daemon's policy",
|
||||
"description": "Fetched on connect and whenever event.settings.changed names a capture.* key, so the two can never disagree about what should be intercepted.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 60,
|
||||
"method": "capture.getRules",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 60,
|
||||
"result": {
|
||||
"enabled": true,
|
||||
"monitoredExtensions": [
|
||||
"iso",
|
||||
"zip",
|
||||
"tar",
|
||||
"gz",
|
||||
"xz",
|
||||
"7z",
|
||||
"rar",
|
||||
"deb",
|
||||
"rpm",
|
||||
"exe",
|
||||
"msi",
|
||||
"appimage",
|
||||
"mkv",
|
||||
"mp4",
|
||||
"flac",
|
||||
"pdf"
|
||||
],
|
||||
"monitoredMimeTypes": [
|
||||
"application/octet-stream",
|
||||
"application/x-iso9660-image",
|
||||
"application/zip",
|
||||
"video/x-matroska"
|
||||
],
|
||||
"minSizeBytes": 1048576,
|
||||
"excludedHosts": [
|
||||
"*.corp.internal",
|
||||
"localhost"
|
||||
],
|
||||
"bypassModifier": "alt",
|
||||
"rulesVersion": 7
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"if this call fails the extension keeps its last known rules and stays fail-open",
|
||||
"rulesVersion increases on every change so the extension knows when to re-fetch"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "category.list \u2014 the five built-in categories",
|
||||
"description": "Read-only and available over both transports: the extension's default-category picker needs it.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 30,
|
||||
"method": "category.list",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 30,
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"categoryId": "compressed",
|
||||
"name": "Compressed",
|
||||
"saveDir": "/home/sami/Downloads/Compressed",
|
||||
"extensions": [
|
||||
"zip",
|
||||
"tar",
|
||||
"gz",
|
||||
"xz",
|
||||
"zst",
|
||||
"7z",
|
||||
"rar"
|
||||
],
|
||||
"mimeTypes": [
|
||||
"application/zip"
|
||||
],
|
||||
"builtin": true,
|
||||
"sortOrder": 0
|
||||
},
|
||||
{
|
||||
"categoryId": "documents",
|
||||
"name": "Documents",
|
||||
"saveDir": "/home/sami/Downloads/Documents",
|
||||
"extensions": [
|
||||
"pdf",
|
||||
"epub",
|
||||
"odt",
|
||||
"docx"
|
||||
],
|
||||
"mimeTypes": [
|
||||
"application/pdf"
|
||||
],
|
||||
"builtin": true,
|
||||
"sortOrder": 1
|
||||
},
|
||||
{
|
||||
"categoryId": "music",
|
||||
"name": "Music",
|
||||
"saveDir": "/home/sami/Downloads/Music",
|
||||
"extensions": [
|
||||
"mp3",
|
||||
"flac",
|
||||
"ogg",
|
||||
"opus",
|
||||
"wav"
|
||||
],
|
||||
"mimeTypes": [
|
||||
"audio/flac"
|
||||
],
|
||||
"builtin": true,
|
||||
"sortOrder": 2
|
||||
},
|
||||
{
|
||||
"categoryId": "programs",
|
||||
"name": "Programs",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"extensions": [
|
||||
"exe",
|
||||
"msi",
|
||||
"deb",
|
||||
"rpm",
|
||||
"appimage",
|
||||
"iso",
|
||||
"dmg"
|
||||
],
|
||||
"mimeTypes": [
|
||||
"application/x-iso9660-image"
|
||||
],
|
||||
"builtin": true,
|
||||
"sortOrder": 3
|
||||
},
|
||||
{
|
||||
"categoryId": "video",
|
||||
"name": "Video",
|
||||
"saveDir": "/home/sami/Downloads/Video",
|
||||
"extensions": [
|
||||
"mkv",
|
||||
"mp4",
|
||||
"avi",
|
||||
"webm",
|
||||
"mov"
|
||||
],
|
||||
"mimeTypes": [
|
||||
"video/mp4",
|
||||
"video/x-matroska"
|
||||
],
|
||||
"builtin": true,
|
||||
"sortOrder": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"built-in categories always exist and cannot be removed",
|
||||
"extensions are lowercase and carry no leading dot"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "category.remove \u2014 delete a user category and refile its tasks",
|
||||
"description": "No task is ever orphaned: everything filed under the removed category moves to reassignTo.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 32,
|
||||
"method": "category.remove",
|
||||
"params": {
|
||||
"categoryId": "firmware",
|
||||
"reassignTo": "programs"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 32,
|
||||
"result": {
|
||||
"removed": true,
|
||||
"reassignedTaskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"removing a builtin category is refused with -32602",
|
||||
"reassignTo null moves tasks to the default category rather than leaving them dangling"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "category.upsert \u2014 create a user category for firmware images",
|
||||
"description": "Omitting categoryId creates; the daemon assigns the id and echoes the stored row back.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 31,
|
||||
"method": "category.upsert",
|
||||
"params": {
|
||||
"category": {
|
||||
"categoryId": "firmware",
|
||||
"name": "Firmware",
|
||||
"saveDir": "/home/sami/Downloads/Firmware",
|
||||
"extensions": [
|
||||
"bin",
|
||||
"img",
|
||||
"fw"
|
||||
],
|
||||
"mimeTypes": [],
|
||||
"builtin": false,
|
||||
"sortOrder": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 31,
|
||||
"result": {
|
||||
"category": {
|
||||
"categoryId": "firmware",
|
||||
"name": "Firmware",
|
||||
"saveDir": "/home/sami/Downloads/Firmware",
|
||||
"extensions": [
|
||||
"bin",
|
||||
"img",
|
||||
"fw"
|
||||
],
|
||||
"mimeTypes": [],
|
||||
"builtin": false,
|
||||
"sortOrder": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"builtin is forced false on a created category regardless of what was sent",
|
||||
"changing saveDir never moves existing files \u2014 the GUI asks and issues download.update per task"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "download.add \u2014 start an ISO now, into the Programs category",
|
||||
"description": "The ordinary add path. saveDir is canonicalized and checked against the allowed roots before anything is written.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 11,
|
||||
"method": "download.add",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"categoryId": "programs",
|
||||
"segments": 8,
|
||||
"startMode": "now"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 11,
|
||||
"result": {
|
||||
"taskId": "$uuid",
|
||||
"state": "connecting",
|
||||
"duplicate": null
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the .veloxpart file is created sparse and preallocated at the final size",
|
||||
"saveDir resolves inside saveTo.allowedRoots, or the call fails -32011 having written nothing",
|
||||
"event.task.added is emitted to every subscriber before this reply is sent"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "download.addBatch \u2014 three URLs, one bad, added together",
|
||||
"description": "Partial success is normal: the two good items become tasks and the third is reported per-item rather than failing the batch.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 12,
|
||||
"method": "download.addBatch",
|
||||
"params": {
|
||||
"items": [
|
||||
{
|
||||
"url": "https://example.org/a.zip"
|
||||
},
|
||||
{
|
||||
"url": "https://example.org/b.zip"
|
||||
},
|
||||
{
|
||||
"url": "https://example.org/c.zip",
|
||||
"saveDir": "/etc"
|
||||
}
|
||||
],
|
||||
"defaults": {
|
||||
"url": "https://example.org/",
|
||||
"categoryId": "compressed",
|
||||
"startMode": "queue",
|
||||
"queueId": "main"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 12,
|
||||
"result": {
|
||||
"taskIds": [
|
||||
"$uuid",
|
||||
"$uuid"
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"index": 2,
|
||||
"code": -32011,
|
||||
"message": "destination is outside the allowed roots"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"defaults fill only fields an item left unset; the defaults' own url is ignored",
|
||||
"one rejected item never rolls back the accepted ones",
|
||||
"failed[].index refers to params.items, so the caller can map it back to its own list"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "download.cancel \u2014 applied to a two-task selection",
|
||||
"description": "The part file is kept: cancel stops the transfer, download.remove is what deletes bytes.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 23,
|
||||
"method": "download.cancel",
|
||||
"params": {
|
||||
"taskIds": [
|
||||
"$taskId",
|
||||
"$taskId2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 23,
|
||||
"result": {
|
||||
"updated": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"state": "cancelled",
|
||||
"changed": true
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"state": "cancelled",
|
||||
"changed": false
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a task already in the target state is reported with changed false, not as a failure",
|
||||
"an unknown id lands in failed[] with -32010 and never fails the whole call",
|
||||
"event.task.state is emitted for every entry whose changed is true"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "download.get \u2014 full detail with per-segment state",
|
||||
"description": "Backs the progress dialog. This is the only place segment-level detail crosses the wire.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 14,
|
||||
"method": "download.get",
|
||||
"params": {
|
||||
"taskId": "$taskId"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 14,
|
||||
"result": {
|
||||
"summary": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"downloadedBytes": 2941190144,
|
||||
"state": "downloading",
|
||||
"speedBps": 29796556,
|
||||
"etaSeconds": 110,
|
||||
"resumable": true,
|
||||
"segments": 8,
|
||||
"categoryId": "programs",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": null,
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": "$isoDate",
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
},
|
||||
"segmentDetail": [
|
||||
{
|
||||
"index": 0,
|
||||
"startByte": 0,
|
||||
"endByte": 778567679,
|
||||
"downloadedBytes": 402653184,
|
||||
"speedBps": 4089446,
|
||||
"state": "receiving",
|
||||
"httpStatus": 206
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"startByte": 778567680,
|
||||
"endByte": 1557135359,
|
||||
"downloadedBytes": 356515840,
|
||||
"speedBps": 3565158,
|
||||
"state": "receiving",
|
||||
"httpStatus": 206
|
||||
}
|
||||
],
|
||||
"headers": {
|
||||
"User-Agent": "Velox/0.1",
|
||||
"Referer": "https://releases.ubuntu.com/26.04/"
|
||||
},
|
||||
"referrer": "https://releases.ubuntu.com/26.04/",
|
||||
"userAgent": "Velox/0.1",
|
||||
"mime": "application/octet-stream",
|
||||
"bufferBytes": 4194304,
|
||||
"partPath": "/home/sami/Downloads/Programs/ubuntu-26.04-desktop-amd64.iso.veloxpart",
|
||||
"checksum": null,
|
||||
"checksumVerified": null,
|
||||
"averageSpeedBps": 27262976,
|
||||
"retryCount": 0
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"segment ranges are contiguous and cover exactly [0, sizeBytes)",
|
||||
"the GUI draws one bar per entry and is never told what a segment steal is"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "download.list \u2014 the main table, unfinished first page",
|
||||
"description": "Filtering, sorting and paging all happen daemon-side; the GUI never materializes rows it will not draw.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 13,
|
||||
"method": "download.list",
|
||||
"params": {
|
||||
"filter": {
|
||||
"states": [
|
||||
"downloading",
|
||||
"paused",
|
||||
"queued"
|
||||
]
|
||||
},
|
||||
"sort": {
|
||||
"field": "createdAt",
|
||||
"direction": "desc"
|
||||
},
|
||||
"offset": 0,
|
||||
"limit": 50
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 13,
|
||||
"result": {
|
||||
"total": 4,
|
||||
"items": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"downloadedBytes": 2941190144,
|
||||
"state": "downloading",
|
||||
"speedBps": 29796556,
|
||||
"etaSeconds": 110,
|
||||
"resumable": true,
|
||||
"segments": 8,
|
||||
"categoryId": "programs",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": null,
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": "$isoDate",
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"filename": "film.mkv",
|
||||
"saveDir": "/home/sami/Downloads/Video",
|
||||
"url": "https://example.org/film.mkv",
|
||||
"effectiveUrl": "https://example.org/film.mkv",
|
||||
"sizeBytes": 1503238553,
|
||||
"downloadedBytes": 402653184,
|
||||
"state": "paused",
|
||||
"speedBps": 0,
|
||||
"etaSeconds": null,
|
||||
"resumable": true,
|
||||
"segments": 4,
|
||||
"categoryId": "video",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": null,
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": "$isoDate",
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"total counts every row matching the filter, ignoring offset and limit",
|
||||
"the table is built from this once and maintained from events thereafter, never re-fetched per tick"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "download.pause \u2014 applied to a two-task selection",
|
||||
"description": "Progress is flushed to .veloxpart.meta, so a pause is indistinguishable from a crash as far as resume is concerned.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 21,
|
||||
"method": "download.pause",
|
||||
"params": {
|
||||
"taskIds": [
|
||||
"$taskId",
|
||||
"$taskId2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 21,
|
||||
"result": {
|
||||
"updated": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"state": "paused",
|
||||
"changed": true
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"state": "paused",
|
||||
"changed": false
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a task already in the target state is reported with changed false, not as a failure",
|
||||
"an unknown id lands in failed[] with -32010 and never fails the whole call",
|
||||
"event.task.state is emitted for every entry whose changed is true"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "download.probe \u2014 a resumable ISO on a well-behaved server",
|
||||
"description": "Populates the File Info dialog. Establishes resumability from Accept-Ranges plus a validator.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 10,
|
||||
"method": "download.probe",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"headers": {
|
||||
"User-Agent": "Velox/0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 10,
|
||||
"result": {
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"mime": "application/octet-stream",
|
||||
"resumable": true,
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"suggestedCategoryId": "programs",
|
||||
"suggestedSaveDir": "/home/sami/Downloads/Programs",
|
||||
"etag": "\"5cf1a2b3-1730f4000\"",
|
||||
"lastModified": "Thu, 23 Apr 2026 10:14:52 GMT",
|
||||
"acceptRanges": true,
|
||||
"redirectChain": [],
|
||||
"requiresAuth": false
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"no task is created by a probe",
|
||||
"resumable is true only when Accept-Ranges: bytes AND a validator are both present",
|
||||
"the dialog opens before this returns; the RPC loop is never blocked on the network"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "download.refreshUrl \u2014 a signed URL expired mid-download",
|
||||
"description": "IDM's Refresh Download Address. Points the task at a fresh link and keeps every byte already on disk.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 26,
|
||||
"method": "download.refreshUrl",
|
||||
"params": {
|
||||
"taskId": "$taskId",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso?token=eyJhbGciOiJIUzI1NiJ9.fresh",
|
||||
"headers": {
|
||||
"Referer": "https://releases.ubuntu.com/26.04/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 26,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"resumable": true,
|
||||
"contentChanged": false,
|
||||
"sizeBytes": 6228541440,
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso?token=eyJhbGciOiJIUzI1NiJ9.fresh"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"size and validator are compared against what was recorded before resuming",
|
||||
"contentChanged true must make the GUI ask before restarting; bytes are never discarded silently"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "download.remove \u2014 drop two tasks and delete their bytes",
|
||||
"description": "The only method that destroys user data, and the reason it is refused over the WebSocket transport.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 24,
|
||||
"method": "download.remove",
|
||||
"params": {
|
||||
"taskIds": [
|
||||
"$taskId",
|
||||
"$taskId2"
|
||||
],
|
||||
"deleteFile": true
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 24,
|
||||
"result": {
|
||||
"removed": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the .veloxpart and .veloxpart.meta pair is always removed",
|
||||
"the finished file is removed only when deleteFile is true",
|
||||
"this call is -32003 over the WebSocket transport"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "download.resume \u2014 applied to a two-task selection",
|
||||
"description": "Revalidated with If-Range against the stored validator before a single byte is appended.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 22,
|
||||
"method": "download.resume",
|
||||
"params": {
|
||||
"taskIds": [
|
||||
"$taskId",
|
||||
"$taskId2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 22,
|
||||
"result": {
|
||||
"updated": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"state": "connecting",
|
||||
"changed": true
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"state": "connecting",
|
||||
"changed": false
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a task already in the target state is reported with changed false, not as a failure",
|
||||
"an unknown id lands in failed[] with -32010 and never fails the whole call",
|
||||
"event.task.state is emitted for every entry whose changed is true"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "download.start \u2014 applied to a two-task selection",
|
||||
"description": "A queued task jumps its queue; one already downloading is a no-op reported as changed false.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 20,
|
||||
"method": "download.start",
|
||||
"params": {
|
||||
"taskIds": [
|
||||
"$taskId",
|
||||
"$taskId2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 20,
|
||||
"result": {
|
||||
"updated": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"state": "connecting",
|
||||
"changed": true
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"state": "connecting",
|
||||
"changed": false
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a task already in the target state is reported with changed false, not as a failure",
|
||||
"an unknown id lands in failed[] with -32010 and never fails the whole call",
|
||||
"event.task.state is emitted for every entry whose changed is true"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "download.update \u2014 refile a task into another category",
|
||||
"description": "Moving saveDir moves the file on disk in the same operation, which is what makes a drag onto the category tree one RPC.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 25,
|
||||
"method": "download.update",
|
||||
"params": {
|
||||
"taskId": "$taskId",
|
||||
"patch": {
|
||||
"categoryId": "video",
|
||||
"saveDir": "/home/sami/Downloads/Video",
|
||||
"description": "Ubuntu 26.04 desktop image"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 25,
|
||||
"result": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Video",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"downloadedBytes": 2941190144,
|
||||
"state": "downloading",
|
||||
"speedBps": 29796556,
|
||||
"etaSeconds": 110,
|
||||
"resumable": true,
|
||||
"segments": 8,
|
||||
"categoryId": "video",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": "Ubuntu 26.04 desktop image",
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": "$isoDate",
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the file and its .veloxpart.meta move together, or neither moves",
|
||||
"the new saveDir is canonicalized and checked against the allowed roots first",
|
||||
"a running task is not re-segmented underneath the user"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "capture.offer \u2014 a monitored type below the minimum size is declined",
|
||||
"description": "Not an error: a normal 'no'. The daemon answers well inside the deadline and the extension lets Firefox handle it. reason is what the popup's diagnostics show.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 111,
|
||||
"method": "capture.offer",
|
||||
"params": {
|
||||
"url": "https://example.org/thumb.zip",
|
||||
"method": "GET",
|
||||
"tabUrl": "https://example.org/gallery",
|
||||
"contentType": "application/zip",
|
||||
"contentLength": 4096,
|
||||
"filename": "thumb.zip",
|
||||
"origin": "moz-extension://11111111-2222-3333-4444-555555555555"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 111,
|
||||
"result": {
|
||||
"action": "ignore",
|
||||
"taskId": null,
|
||||
"reason": "below_min_size"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"action ignore means the extension returns {} and Firefox downloads normally",
|
||||
"the answer still arrives within 750 ms",
|
||||
"reason is set on every ignore so a puzzled user can find out why"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "capture.offer \u2014 the daemon does not answer within 750 ms",
|
||||
"description": "The most important test in this directory. The daemon is slow, wedged or gone; the extension must abandon the offer and let Firefox download the file normally. A download manager that eats downloads when its daemon is down is worse than no download manager, and this behaviour is non-negotiable.",
|
||||
"kind": "timeout",
|
||||
"deadlineMs": 750,
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 110,
|
||||
"method": "capture.offer",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"method": "GET",
|
||||
"tabUrl": "https://releases.ubuntu.com/26.04/",
|
||||
"contentType": "application/octet-stream",
|
||||
"contentLength": 6228541440,
|
||||
"origin": "moz-extension://11111111-2222-3333-4444-555555555555"
|
||||
}
|
||||
},
|
||||
"response": null,
|
||||
"assertions": [
|
||||
"the extension gives up at 750 ms measured from send, not from connect",
|
||||
"webRequest returns {} so Firefox downloads the file itself",
|
||||
"no task is created, and the user sees the download in Firefox's own list",
|
||||
"the extension does not retry: a retry would race the browser's own download",
|
||||
"the same behaviour applies when the transport is disconnected entirely",
|
||||
"reproduced against mockd with --slow 2000, and by killing the daemon outright"
|
||||
],
|
||||
"requires": "a daemon that is slow, wedged or absent"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "download.add \u2014 not enough space to preallocate",
|
||||
"description": "Preallocating at the final size means the failure happens now, on add, rather than at 97 % after an hour.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 106,
|
||||
"method": "download.add",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"startMode": "now"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 106,
|
||||
"error": {
|
||||
"code": -32012,
|
||||
"message": "not enough free space to preallocate 5.8 GB"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the check is against the actual filesystem holding saveDir, not the home directory",
|
||||
"the partially created .veloxpart is removed before this error is returned",
|
||||
"not replayable against a mock: exercised in tests/integration on a small tmpfs"
|
||||
],
|
||||
"requires": "a filesystem with no free space"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "download.add \u2014 segments above the contract's maximum",
|
||||
"description": "Params are validated against the schema before any handler runs, so a handler never sees an out-of-range value.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 108,
|
||||
"method": "download.add",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"segments": 64,
|
||||
"startMode": "now"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 108,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "params/segments: value is above the maximum of 32",
|
||||
"data": {
|
||||
"path": "params/segments"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"data.path is a JSON Pointer at the offending field",
|
||||
"validation happens before the handler, on both transports"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "download.add \u2014 a destination that escapes the allowed roots",
|
||||
"description": "Paths are canonicalized before the check, so ../ traversal and symlinks cannot smuggle a write outside saveTo.allowedRoots.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 105,
|
||||
"method": "download.add",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/../../etc",
|
||||
"startMode": "now"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 105,
|
||||
"error": {
|
||||
"code": -32011,
|
||||
"message": "destination is outside the allowed roots",
|
||||
"data": {
|
||||
"path": "/home/sami/Downloads/../../etc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the path is canonicalized first: the check is on the resolved path, never the literal string",
|
||||
"no file, no .veloxpart and no database row is created",
|
||||
"a symlink whose target escapes the roots is refused the same way"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "download.get \u2014 an unknown task id",
|
||||
"description": "The ordinary stale-client case: the GUI asks about a row another client has since removed.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 104,
|
||||
"method": "download.get",
|
||||
"params": {
|
||||
"taskId": "00000000-0000-4000-8000-000000000000"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 104,
|
||||
"error": {
|
||||
"code": -32010,
|
||||
"message": "no such task",
|
||||
"data": {
|
||||
"taskId": "00000000-0000-4000-8000-000000000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a bulk method reports this per id in failed[] instead of failing the whole call",
|
||||
"the client's correct response is to drop the row, not to retry"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "download.probe \u2014 the server answered 403",
|
||||
"description": "data.httpStatus is what lets the GUI say 'the link has expired' instead of 'probe failed'.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 107,
|
||||
"method": "download.probe",
|
||||
"params": {
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso?token=expired"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 107,
|
||||
"error": {
|
||||
"code": -32013,
|
||||
"message": "probe failed: HTTP 403",
|
||||
"data": {
|
||||
"httpStatus": 403
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"data.httpStatus is present whenever there was an HTTP response at all",
|
||||
"a DNS or connection failure returns -32013 with httpStatus null",
|
||||
"no task is created by a failed probe",
|
||||
"not replayable against a mock: exercised in tests/integration against tools/testserver"
|
||||
],
|
||||
"requires": "an origin server that answers 403"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "an unknown method name",
|
||||
"description": "Version skew and local port-scanning both look like this. Neither gets a useful reply.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 109,
|
||||
"method": "download.deleteEverything",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 109,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "no such method"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the reply does not enumerate valid methods",
|
||||
"an unknown method never closes the connection: a newer client may simply be probing for a capability"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "session.hello \u2014 an unpaired WebSocket client is refused",
|
||||
"description": "The WS transport is reachable by any local process, so a token is mandatory there. The Unix socket needs none: SO_PEERCRED already proved same-UID.",
|
||||
"transport": "ws",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 101,
|
||||
"method": "session.hello",
|
||||
"params": {
|
||||
"clientType": "extension",
|
||||
"clientName": "Velox for Firefox",
|
||||
"protocolVersion": "1.0.0",
|
||||
"token": "not-a-real-token"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 101,
|
||||
"error": {
|
||||
"code": -32002,
|
||||
"message": "not paired: call session.pair first"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"only session.pair is served on an unauthenticated WebSocket connection",
|
||||
"the reply does not reveal whether the token was absent, malformed or merely wrong",
|
||||
"this failure counts towards the pairing rate limit"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "session.hello \u2014 a client built against protocol 2.x is refused",
|
||||
"description": "Major mismatch fails loudly at connect rather than subtly at the tenth field. The GUI renders this as 'Velox needs updating'.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 100,
|
||||
"method": "session.hello",
|
||||
"params": {
|
||||
"clientType": "gui",
|
||||
"clientName": "velox-gui 9.9.9",
|
||||
"protocolVersion": "2.0.0"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 100,
|
||||
"error": {
|
||||
"code": -32001,
|
||||
"message": "protocol major version mismatch: daemon speaks 1.x, client speaks 2.x",
|
||||
"data": {
|
||||
"expected": "1.0.0",
|
||||
"actual": "2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the connection is closed after this reply; no method is served on a mismatched major",
|
||||
"a differing minor or patch is accepted, never refused",
|
||||
"the message is safe to show a user verbatim",
|
||||
"the version check is transport-independent; this is replayed on the Unix socket so it is not masked by -32002"
|
||||
],
|
||||
"transport": "uds"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "session.pair \u2014 the sixth failed attempt in a minute is locked out",
|
||||
"description": "Rate limiting is what stops another local process brute-forcing its way to a token.",
|
||||
"transport": "ws",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 102,
|
||||
"method": "session.pair",
|
||||
"params": {
|
||||
"clientName": "Velox for Firefox",
|
||||
"extensionId": "11111111-2222-3333-4444-555555555555",
|
||||
"code": "0000"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 102,
|
||||
"error": {
|
||||
"code": -32014,
|
||||
"message": "too many pairing attempts; try again later",
|
||||
"data": {
|
||||
"retryAfterSec": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"five failures per minute, then a 60 s lockout",
|
||||
"the lockout is per-origin and survives a reconnect, or it is not a lockout",
|
||||
"no user prompt is shown while locked out \u2014 the prompt itself is the thing being flooded",
|
||||
"the runner does not brute-force a live daemon; mockd reproduces it under --lockout"
|
||||
],
|
||||
"requires": "six failed pairing attempts inside one minute"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "settings.set \u2014 a privileged method called over the WebSocket transport",
|
||||
"description": "The extension may request a download; it may not reconfigure the daemon. Letting it write saveTo.allowedRoots would defeat every path check in the project.",
|
||||
"transport": "ws",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 103,
|
||||
"method": "settings.set",
|
||||
"params": {
|
||||
"values": {
|
||||
"saveTo.allowedRoots": [
|
||||
"/"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 103,
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "method is not permitted on this transport"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the check happens before params are even parsed",
|
||||
"every method with x-privileged true behaves identically here",
|
||||
"nothing is written and no event is emitted"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "event.auth.required \u2014 a server wants Basic credentials",
|
||||
"description": "The task waits in retry_wait until the client supplies them. Credentials go to the Secret Service, never back through this event and never into a log.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.auth.required",
|
||||
"params": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"host": "files.example.org",
|
||||
"realm": "Restricted",
|
||||
"scheme": "basic"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"no credential material appears in this payload",
|
||||
"the task sits in retry_wait rather than failing outright"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "event.grabber.progress \u2014 a crawl in flight",
|
||||
"description": "done true means the file list in grabber.status is final.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.grabber.progress",
|
||||
"params": {
|
||||
"jobId": "grab-7f21",
|
||||
"found": 3,
|
||||
"crawled": 12,
|
||||
"done": false,
|
||||
"currentUrl": "https://releases.ubuntu.com/26.04/"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"emitted at no more than 4 Hz",
|
||||
"the wizard shows found and crawled separately: they diverge on a deep crawl"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "event.notify \u2014 a download finished",
|
||||
"description": "The client decides between a toast, a tray balloon and a sound; the daemon does not assume a GUI is running.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.notify",
|
||||
"params": {
|
||||
"level": "success",
|
||||
"title": "Download complete",
|
||||
"body": "ubuntu-26.04-desktop-amd64.iso (5.8 GB) finished in 3 m 28 s.",
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"sound": "complete"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the daemon never assumes a GUI is running to see this",
|
||||
"sound names an event, not a file path \u2014 the client owns its sound set"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "event.settings.changed \u2014 capture policy was edited",
|
||||
"description": "Carries only key names. The extension watches for capture.* here and re-fetches capture.getRules so its mirror never lags the daemon.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.settings.changed",
|
||||
"params": {
|
||||
"keys": [
|
||||
"capture.monitoredExtensions",
|
||||
"capture.minSizeBytes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a client re-reads only the keys it cares about",
|
||||
"the extension treats any capture.* key as a signal to call capture.getRules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "event.speed.global \u2014 the status bar's 1 Hz tick",
|
||||
"description": "Emitted even when nothing is active, so a client can tell 'idle' from 'disconnected'.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.speed.global",
|
||||
"params": {
|
||||
"downBps": 29796556,
|
||||
"activeCount": 1,
|
||||
"queuedCount": 2,
|
||||
"limitBps": null
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"emitted at 1 Hz whether or not anything is downloading",
|
||||
"limitBps null means the limiter is off"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "event.task.added \u2014 a new row appears",
|
||||
"description": "summary is always present so a client can insert the row without a follow-up download.get.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.task.added",
|
||||
"params": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"summary": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"downloadedBytes": 0,
|
||||
"state": "queued",
|
||||
"speedBps": 0,
|
||||
"etaSeconds": null,
|
||||
"resumable": true,
|
||||
"segments": 8,
|
||||
"categoryId": "programs",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": null,
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": null,
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"every subscriber sees this, including the client that made the download.add call",
|
||||
"the row can be drawn from this payload alone"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "event.task.progress \u2014 one batched tick for two tasks",
|
||||
"description": "A single array at no more than 4 Hz, never one notification per task. At twenty active downloads that is four messages a second instead of eighty.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.task.progress",
|
||||
"params": {
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"downloadedBytes": 2941190144,
|
||||
"speedBps": 29796556,
|
||||
"etaSeconds": 110,
|
||||
"segments": [
|
||||
{
|
||||
"index": 0,
|
||||
"downloadedBytes": 402653184,
|
||||
"speedBps": 4089446
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"downloadedBytes": 356515840,
|
||||
"speedBps": 3565158
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"taskId": "8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"downloadedBytes": 402653184,
|
||||
"speedBps": 0,
|
||||
"etaSeconds": null,
|
||||
"segments": []
|
||||
}
|
||||
],
|
||||
"at": "$isoDate"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"emitted at no more than 4 Hz regardless of how many tasks are active",
|
||||
"clients apply a row patch; rebuilding the model on this event is a bug",
|
||||
"a task with no segment detail still reports its byte counter"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "event.task.removed \u2014 a row disappears",
|
||||
"description": "There is nothing further to fetch: the client deletes the row.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.task.removed",
|
||||
"params": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"deletedFile": true
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"deletedFile tells the GUI whether to offer an undo that is still meaningful"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "event.task.state \u2014 a task fails on a dead link",
|
||||
"description": "Carries the summary so the row repaints in full, and the error whenever the new state is failed or retry_wait.",
|
||||
"notification": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event.task.state",
|
||||
"params": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"state": "failed",
|
||||
"previousState": "downloading",
|
||||
"summary": {
|
||||
"taskId": "3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"saveDir": "/home/sami/Downloads/Programs",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"effectiveUrl": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"downloadedBytes": 0,
|
||||
"state": "failed",
|
||||
"speedBps": 0,
|
||||
"etaSeconds": null,
|
||||
"resumable": true,
|
||||
"segments": 8,
|
||||
"categoryId": "programs",
|
||||
"queueId": null,
|
||||
"queuePosition": null,
|
||||
"description": null,
|
||||
"createdAt": "$isoDate",
|
||||
"lastTryAt": "$isoDate",
|
||||
"completedAt": null,
|
||||
"error": {
|
||||
"code": -32013,
|
||||
"message": "HTTP 404 on resume",
|
||||
"httpStatus": 404,
|
||||
"retryable": false,
|
||||
"attempt": 3,
|
||||
"nextRetryAt": null
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"code": -32013,
|
||||
"message": "HTTP 404 on resume",
|
||||
"httpStatus": 404,
|
||||
"retryable": false,
|
||||
"attempt": 3,
|
||||
"nextRetryAt": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"error is present exactly when state is failed or retry_wait",
|
||||
"retryable false means the scheduler will not pick this up again on its own"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "grabber.harvest \u2014 download the two selected ISOs",
|
||||
"description": "The only grabber call that creates tasks, and it names exactly what the user ticked.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 72,
|
||||
"method": "grabber.harvest",
|
||||
"params": {
|
||||
"jobId": "grab-7f21",
|
||||
"select": [
|
||||
"f1",
|
||||
"f2"
|
||||
],
|
||||
"defaults": {
|
||||
"url": "https://releases.ubuntu.com/26.04/",
|
||||
"categoryId": "programs",
|
||||
"startMode": "queue",
|
||||
"queueId": "main"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 72,
|
||||
"result": {
|
||||
"taskIds": [
|
||||
"$uuid",
|
||||
"$uuid"
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"only the selected fileIds become tasks",
|
||||
"a fileId that is not in the job is reported in failed[], never silently skipped"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "grabber.start \u2014 crawl a release directory two levels deep",
|
||||
"description": "Nothing is downloaded by this call. It walks pages and collects candidates for the wizard to show.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 70,
|
||||
"method": "grabber.start",
|
||||
"params": {
|
||||
"startUrl": "https://releases.ubuntu.com/26.04/",
|
||||
"depth": 2,
|
||||
"includePatterns": [
|
||||
"*/26.04/*"
|
||||
],
|
||||
"excludePatterns": [
|
||||
"*/torrent/*"
|
||||
],
|
||||
"fileTypes": [
|
||||
"iso",
|
||||
"zsync"
|
||||
],
|
||||
"sameHostOnly": true,
|
||||
"maxFiles": 200
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 70,
|
||||
"result": {
|
||||
"jobId": "grab-7f21"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a crawl never starts a download on its own",
|
||||
"sameHostOnly true keeps the crawl off third-party hosts linked from the page"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "grabber.status \u2014 crawl finished with three candidates",
|
||||
"description": "Also delivered as event.grabber.progress; the poll exists so a reopened wizard can catch up.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 71,
|
||||
"method": "grabber.status",
|
||||
"params": {
|
||||
"jobId": "grab-7f21"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 71,
|
||||
"result": {
|
||||
"jobId": "grab-7f21",
|
||||
"state": "done",
|
||||
"crawled": 12,
|
||||
"found": 3,
|
||||
"files": [
|
||||
{
|
||||
"fileId": "f1",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||
"sizeBytes": 6228541440,
|
||||
"contentType": "application/octet-stream",
|
||||
"depth": 1,
|
||||
"foundOn": "https://releases.ubuntu.com/26.04/"
|
||||
},
|
||||
{
|
||||
"fileId": "f2",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-live-server-amd64.iso",
|
||||
"filename": "ubuntu-26.04-live-server-amd64.iso",
|
||||
"sizeBytes": 2617245696,
|
||||
"contentType": "application/octet-stream",
|
||||
"depth": 1,
|
||||
"foundOn": "https://releases.ubuntu.com/26.04/"
|
||||
},
|
||||
{
|
||||
"fileId": "f3",
|
||||
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso.zsync",
|
||||
"filename": "ubuntu-26.04-desktop-amd64.iso.zsync",
|
||||
"sizeBytes": 12189696,
|
||||
"contentType": "application/octet-stream",
|
||||
"depth": 1,
|
||||
"foundOn": "https://releases.ubuntu.com/26.04/"
|
||||
}
|
||||
],
|
||||
"error": null
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"state done means the file list is final",
|
||||
"sizeBytes is null where the server refused a HEAD, and the wizard must cope with that"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "limiter.get \u2014 the limiter is off",
|
||||
"description": "globalBps still carries the last configured value so the GUI can restore it when the user re-enables the limit.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 52,
|
||||
"method": "limiter.get",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 52,
|
||||
"result": {
|
||||
"enabled": false,
|
||||
"globalBps": 2097152,
|
||||
"applyToRunning": false
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"enabled false means no throttling regardless of globalBps"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "limiter.set \u2014 cap at 2 MiB/s and retune what is already running",
|
||||
"description": "applyToRunning true is the Speed Limiter window's 'apply now' button.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"method": "limiter.set",
|
||||
"params": {
|
||||
"enabled": true,
|
||||
"globalBps": 2097152,
|
||||
"applyToRunning": true
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"result": {
|
||||
"enabled": true,
|
||||
"globalBps": 2097152,
|
||||
"applyToRunning": true
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the limit is global across every active task, not per task",
|
||||
"with applyToRunning true, transfers already in flight are retuned rather than waiting for the next task"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "media.addVariant \u2014 download 1080p with the English audio muxed in",
|
||||
"description": "The result is an ordinary task that appears in the list like any other download.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 62,
|
||||
"method": "media.addVariant",
|
||||
"params": {
|
||||
"manifestUrl": "https://cdn.example.org/v/master.m3u8",
|
||||
"variantId": "v-1080p",
|
||||
"audioVariantId": "a-en",
|
||||
"spec": {
|
||||
"url": "https://cdn.example.org/v/master.m3u8",
|
||||
"filename": "episode-42.mkv",
|
||||
"categoryId": "video",
|
||||
"startMode": "now"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 62,
|
||||
"result": {
|
||||
"taskId": "$uuid",
|
||||
"state": "connecting",
|
||||
"estimatedBytes": 1923000000
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"spec.url is ignored: the manifest and variant determine the source",
|
||||
"a DRM-protected variant is refused with -32602 rather than started and failed later",
|
||||
"segments are fetched in parallel and muxed with ffmpeg into one container"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "media.listVariants \u2014 an HLS master playlist",
|
||||
"description": "The daemon parses the manifest; the extension never does. Keeping that logic in one language is the whole point.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 61,
|
||||
"method": "media.listVariants",
|
||||
"params": {
|
||||
"manifestUrl": "https://cdn.example.org/v/master.m3u8",
|
||||
"headers": {
|
||||
"Referer": "https://example.org/watch/42"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 61,
|
||||
"result": {
|
||||
"variants": [
|
||||
{
|
||||
"variantId": "v-1080p",
|
||||
"kind": "video",
|
||||
"resolution": "1920x1080",
|
||||
"bitrateBps": 5000000,
|
||||
"codec": "avc1.640028",
|
||||
"container": "ts",
|
||||
"frameRate": 25.0,
|
||||
"language": null,
|
||||
"sizeEstimate": 1875000000,
|
||||
"drm": false
|
||||
},
|
||||
{
|
||||
"variantId": "v-720p",
|
||||
"kind": "video",
|
||||
"resolution": "1280x720",
|
||||
"bitrateBps": 2800000,
|
||||
"codec": "avc1.4d401f",
|
||||
"container": "ts",
|
||||
"frameRate": 25.0,
|
||||
"language": null,
|
||||
"sizeEstimate": 1050000000,
|
||||
"drm": false
|
||||
},
|
||||
{
|
||||
"variantId": "a-en",
|
||||
"kind": "audio",
|
||||
"resolution": null,
|
||||
"bitrateBps": 128000,
|
||||
"codec": "mp4a.40.2",
|
||||
"container": "ts",
|
||||
"frameRate": null,
|
||||
"language": "en",
|
||||
"sizeEstimate": 48000000,
|
||||
"drm": false
|
||||
}
|
||||
],
|
||||
"manifestType": "hls",
|
||||
"durationSec": 3000.0,
|
||||
"title": "Episode 42",
|
||||
"drmProtected": false
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"sizeEstimate is bitrate x duration and must be labelled as approximate in the UI",
|
||||
"a DRM-protected variant is reported with drm true and greyed out, never attempted"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "queue.list \u2014 two queues, one running",
|
||||
"description": "Not privileged: the extension's Add to Queue picker needs it.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 33,
|
||||
"method": "queue.list",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 33,
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"queueId": "main",
|
||||
"name": "Main Queue",
|
||||
"state": "running",
|
||||
"maxConcurrent": 3,
|
||||
"taskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
],
|
||||
"schedule": null,
|
||||
"onComplete": "nothing"
|
||||
},
|
||||
{
|
||||
"queueId": "sync",
|
||||
"name": "Sync Queue",
|
||||
"state": "stopped",
|
||||
"maxConcurrent": 1,
|
||||
"taskIds": [],
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
},
|
||||
"onComplete": "nothing"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"taskIds are in run order, not insertion order"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "queue.reorder \u2014 move the second task to the front",
|
||||
"description": "taskIds must be a permutation of current membership; anything else is -32602 so a stale drag cannot reshuffle the queue.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 37,
|
||||
"method": "queue.reorder",
|
||||
"params": {
|
||||
"queueId": "main",
|
||||
"taskIds": [
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 37,
|
||||
"result": {
|
||||
"queue": {
|
||||
"queueId": "main",
|
||||
"name": "Main Queue",
|
||||
"state": "running",
|
||||
"maxConcurrent": 3,
|
||||
"taskIds": [
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071"
|
||||
],
|
||||
"schedule": null,
|
||||
"onComplete": "nothing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a taskIds list that is not an exact permutation is rejected whole, never applied partially"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "queue.start \u2014 admit up to maxConcurrent tasks",
|
||||
"description": "The scheduler keeps maxConcurrent running until the queue drains or is stopped.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 35,
|
||||
"method": "queue.start",
|
||||
"params": {
|
||||
"queueId": "main"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 35,
|
||||
"result": {
|
||||
"queue": {
|
||||
"queueId": "main",
|
||||
"name": "Main Queue",
|
||||
"state": "running",
|
||||
"maxConcurrent": 3,
|
||||
"taskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
],
|
||||
"schedule": null,
|
||||
"onComplete": "nothing"
|
||||
},
|
||||
"startedTaskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"no more than maxConcurrent tasks from this queue are ever running at once",
|
||||
"tasks are admitted in queue order, not by size or arrival"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "queue.stop \u2014 stop the queue and pause what is running",
|
||||
"description": "pauseRunning false would let in-flight tasks finish: the difference between stopping a queue and stopping everything.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 36,
|
||||
"method": "queue.stop",
|
||||
"params": {
|
||||
"queueId": "main",
|
||||
"pauseRunning": true
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 36,
|
||||
"result": {
|
||||
"queue": {
|
||||
"queueId": "main",
|
||||
"name": "Main Queue",
|
||||
"state": "stopped",
|
||||
"maxConcurrent": 3,
|
||||
"taskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
],
|
||||
"schedule": null,
|
||||
"onComplete": "nothing"
|
||||
},
|
||||
"pausedTaskIds": [
|
||||
"3f7a2b1c-5d6e-4f80-9a1b-2c3d4e5f6071",
|
||||
"8c1d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"with pauseRunning false, running tasks finish and only admission stops"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "queue.upsert \u2014 set a queue's concurrency and overnight window",
|
||||
"description": "taskIds in the payload is ignored; membership moves through download.update and queue.reorder so two clients editing at once cannot drop a task.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 34,
|
||||
"method": "queue.upsert",
|
||||
"params": {
|
||||
"queue": {
|
||||
"queueId": "sync",
|
||||
"name": "Sync Queue",
|
||||
"state": "stopped",
|
||||
"maxConcurrent": 2,
|
||||
"taskIds": [],
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
},
|
||||
"onComplete": "nothing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 34,
|
||||
"result": {
|
||||
"queue": {
|
||||
"queueId": "sync",
|
||||
"name": "Sync Queue",
|
||||
"state": "stopped",
|
||||
"maxConcurrent": 2,
|
||||
"taskIds": [],
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
},
|
||||
"onComplete": "nothing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the taskIds sent by the client are ignored and the stored order is echoed back",
|
||||
"schedule times are stored as local wall-clock and re-evaluated on a DST change"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "rules.list \u2014 the routing table in priority order",
|
||||
"description": "Privileged: this is the daemon's policy. The extension gets its narrowed view from capture.getRules.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 38,
|
||||
"method": "rules.list",
|
||||
"params": {}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 38,
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"ruleId": "iso-to-programs",
|
||||
"name": "Disk images",
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"match": {
|
||||
"extensions": [
|
||||
"iso",
|
||||
"img"
|
||||
],
|
||||
"mimeTypes": null,
|
||||
"hostPattern": null,
|
||||
"urlPattern": null,
|
||||
"minSizeBytes": null,
|
||||
"maxSizeBytes": null
|
||||
},
|
||||
"action": {
|
||||
"categoryId": "programs",
|
||||
"saveDir": null,
|
||||
"queueId": null,
|
||||
"segments": 8,
|
||||
"startMode": null,
|
||||
"capture": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ruleId": "never-intranet",
|
||||
"name": "Never capture the intranet",
|
||||
"enabled": true,
|
||||
"priority": 20,
|
||||
"match": {
|
||||
"extensions": null,
|
||||
"mimeTypes": null,
|
||||
"hostPattern": "*.corp.internal",
|
||||
"urlPattern": null,
|
||||
"minSizeBytes": null,
|
||||
"maxSizeBytes": null
|
||||
},
|
||||
"action": {
|
||||
"categoryId": null,
|
||||
"saveDir": null,
|
||||
"queueId": null,
|
||||
"segments": null,
|
||||
"startMode": null,
|
||||
"capture": "ignore"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"items are ordered by priority ascending; first match wins",
|
||||
"no rule matching means the default category, never an error"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "rules.upsert \u2014 add one rule and drop another atomically",
|
||||
"description": "Applying adds and removes in one write means a reprioritisation never leaves the table half-valid.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 39,
|
||||
"method": "rules.upsert",
|
||||
"params": {
|
||||
"upsert": [
|
||||
{
|
||||
"ruleId": "big-to-queue",
|
||||
"name": "Queue anything over 1 GiB",
|
||||
"enabled": true,
|
||||
"priority": 5,
|
||||
"match": {
|
||||
"extensions": null,
|
||||
"mimeTypes": null,
|
||||
"hostPattern": null,
|
||||
"urlPattern": null,
|
||||
"minSizeBytes": 1073741824,
|
||||
"maxSizeBytes": null
|
||||
},
|
||||
"action": {
|
||||
"categoryId": null,
|
||||
"saveDir": null,
|
||||
"queueId": "main",
|
||||
"segments": null,
|
||||
"startMode": "queue",
|
||||
"capture": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"remove": [
|
||||
"never-intranet"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 39,
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"ruleId": "big-to-queue",
|
||||
"name": "Queue anything over 1 GiB",
|
||||
"enabled": true,
|
||||
"priority": 5,
|
||||
"match": {
|
||||
"extensions": null,
|
||||
"mimeTypes": null,
|
||||
"hostPattern": null,
|
||||
"urlPattern": null,
|
||||
"minSizeBytes": 1073741824,
|
||||
"maxSizeBytes": null
|
||||
},
|
||||
"action": {
|
||||
"categoryId": null,
|
||||
"saveDir": null,
|
||||
"queueId": "main",
|
||||
"segments": null,
|
||||
"startMode": "queue",
|
||||
"capture": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ruleId": "iso-to-programs",
|
||||
"name": "Disk images",
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"match": {
|
||||
"extensions": [
|
||||
"iso",
|
||||
"img"
|
||||
],
|
||||
"mimeTypes": null,
|
||||
"hostPattern": null,
|
||||
"urlPattern": null,
|
||||
"minSizeBytes": null,
|
||||
"maxSizeBytes": null
|
||||
},
|
||||
"action": {
|
||||
"categoryId": "programs",
|
||||
"saveDir": null,
|
||||
"queueId": null,
|
||||
"segments": 8,
|
||||
"startMode": null,
|
||||
"capture": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the whole table comes back in priority order so the caller need not re-list",
|
||||
"adds and removes commit together or not at all"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "schedule.get \u2014 every queue's schedule",
|
||||
"description": "queueId null asks for all of them. Backs the Scheduler window.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 40,
|
||||
"method": "schedule.get",
|
||||
"params": {
|
||||
"queueId": null
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 40,
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"queueId": "main",
|
||||
"schedule": null
|
||||
},
|
||||
{
|
||||
"queueId": "sync",
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a null schedule means the queue is under manual control"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "schedule.set \u2014 run the sync queue on weekday nights",
|
||||
"description": "Times are local wall-clock and are re-evaluated on a DST change rather than resolved to absolute instants now.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 41,
|
||||
"method": "schedule.set",
|
||||
"params": {
|
||||
"queueId": "sync",
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 41,
|
||||
"result": {
|
||||
"queueId": "sync",
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "periodic",
|
||||
"startTime": "02:00",
|
||||
"stopTime": "06:00",
|
||||
"daysOfWeek": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"onceDate": null
|
||||
},
|
||||
"nextRunAt": "$isoDate"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"nextRunAt is computed in local time and skips days not in daysOfWeek",
|
||||
"a null schedule clears it and leaves the queue manual"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "session.hello \u2014 a GUI client connects over the Unix socket",
|
||||
"description": "The handshake every client makes first. Matching majors, so the daemon answers with its capability list.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "session.hello",
|
||||
"params": {
|
||||
"clientType": "gui",
|
||||
"clientName": "velox-gui 0.1.0",
|
||||
"protocolVersion": "1.0.0"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"daemonVersion": "0.1.0",
|
||||
"protocolVersion": "1.0.0",
|
||||
"capabilities": [
|
||||
"media",
|
||||
"grabber",
|
||||
"secretservice"
|
||||
],
|
||||
"sessionId": "$uuid",
|
||||
"transport": "uds"
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the reply names a protocolVersion whose major matches the request's",
|
||||
"sessionId is a fresh uuid per connection, not per client",
|
||||
"no token is required on the Unix socket: SO_PEERCRED already proved same-UID",
|
||||
"replayed on the Unix socket only: the same call without a token is -32002 over the WebSocket"
|
||||
],
|
||||
"transport": "uds"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "session.pair \u2014 the extension pairs over the WebSocket transport",
|
||||
"description": "First run only. The daemon prompts the user and returns a 256-bit token once they approve.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "session.pair",
|
||||
"params": {
|
||||
"clientName": "Velox for Firefox",
|
||||
"extensionId": "11111111-2222-3333-4444-555555555555"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"token": "$opaque",
|
||||
"expiresAt": null
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"a user prompt is shown before any token is issued \u2014 never auto-approve",
|
||||
"the token is at least 256 bits of entropy, base64url encoded",
|
||||
"the daemon stores a hash of the token, never the token itself",
|
||||
"this method is refused with -32003 on the Unix socket"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "session.subscribe \u2014 the GUI asks for the full event set",
|
||||
"description": "Nothing is delivered until this is called. Subscribing replaces any previous selection.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "session.subscribe",
|
||||
"params": {
|
||||
"events": [
|
||||
"event.task.added",
|
||||
"event.task.removed",
|
||||
"event.task.state",
|
||||
"event.task.progress",
|
||||
"event.speed.global",
|
||||
"event.notify"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"events": [
|
||||
"event.task.added",
|
||||
"event.task.removed",
|
||||
"event.task.state",
|
||||
"event.task.progress",
|
||||
"event.speed.global",
|
||||
"event.notify"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"the echoed list lets a client spot an event it asked for that this daemon does not emit",
|
||||
"no notification arrives on this connection before this call succeeds"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "settings.get \u2014 the Connection tab's keys",
|
||||
"description": "Privileged: the settings bag names local paths and the allowed write roots, which the extension has no business enumerating.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 50,
|
||||
"method": "settings.get",
|
||||
"params": {
|
||||
"keys": [
|
||||
"connection.maxSegmentsPerDownload",
|
||||
"connection.bufferBytes",
|
||||
"connection.maxConcurrentDownloads",
|
||||
"connection.timeoutSec"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 50,
|
||||
"result": {
|
||||
"values": {
|
||||
"connection.maxSegmentsPerDownload": 8,
|
||||
"connection.bufferBytes": 4194304,
|
||||
"connection.maxConcurrentDownloads": 5,
|
||||
"connection.timeoutSec": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"only the requested keys come back",
|
||||
"keys null returns everything",
|
||||
"no password is ever present: credentials live in the Secret Service"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "settings.set \u2014 raise the segment cap and turn on checksums",
|
||||
"description": "Only the keys present change, and changed[] names exactly what took effect.",
|
||||
"request": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 51,
|
||||
"method": "settings.set",
|
||||
"params": {
|
||||
"values": {
|
||||
"connection.maxSegmentsPerDownload": 16,
|
||||
"downloads.verifyChecksums": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 51,
|
||||
"result": {
|
||||
"values": {
|
||||
"connection.maxSegmentsPerDownload": 16,
|
||||
"downloads.verifyChecksums": true
|
||||
},
|
||||
"changed": [
|
||||
"connection.maxSegmentsPerDownload",
|
||||
"downloads.verifyChecksums"
|
||||
]
|
||||
}
|
||||
},
|
||||
"assertions": [
|
||||
"event.settings.changed is emitted carrying exactly the keys in changed[]",
|
||||
"an unknown key is -32602 and nothing at all is written",
|
||||
"a directory key naming an unwritable path is -32011"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user