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:
2026-09-09 19:55:54 +04:00
co-authored by Claude Opus 5
parent a40585f419
commit 53421d6cb8
171 changed files with 29275 additions and 51 deletions
+83
View File
@@ -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.
+52
View File
@@ -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"
]
}
+111
View File
@@ -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"
]
}
+27
View File
@@ -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"
]
}
+47
View File
@@ -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"
]
}
+31
View File
@@ -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"
]
}
+51
View File
@@ -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"
]
}
+39
View File
@@ -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"
]
}
+77
View File
@@ -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"
]
}
+81
View File
@@ -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"
]
}
+39
View File
@@ -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"
]
}
+38
View File
@@ -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"
]
}
+32
View File
@@ -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"
]
}
+39
View File
@@ -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"
]
}
+39
View File
@@ -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"
]
}
+48
View File
@@ -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"
]
}
+37
View File
@@ -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"
]
}
+36
View File
@@ -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"
]
}
+56
View File
@@ -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"
]
}
+22
View File
@@ -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"
]
}
+27
View File
@@ -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"
]
}
+34
View File
@@ -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"
]
}
+55
View File
@@ -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"
]
}
+37
View File
@@ -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"
]
}
+38
View File
@@ -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"
]
}
+38
View File
@@ -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"
]
}
+65
View File
@@ -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"
]
}
+69
View File
@@ -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"
]
}
+97
View File
@@ -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"
]
}
+44
View File
@@ -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"
]
}
+52
View File
@@ -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"
]
}
+36
View File
@@ -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"
}
+27
View File
@@ -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"
]
}
+38
View File
@@ -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"
]
}
+34
View File
@@ -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"
]
}
+34
View File
@@ -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"
]
}