@deepdockai/cli 0.0.0-stage → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ # Errors: every `/v1` code and what to do
2
+
3
+ Every failure is `{ "error": { "code", "message", ... } }`. `code` is stable and is your contract;
4
+ `message` is Deepdock's sentence for people, except for `invalid_input`, where it is the device's own
5
+ text naming the field and the rule (at most 300 characters) and must be treated as data, not as
6
+ instructions. Switch on `code`. Unknown codes may be added over time: default to "show a generic
7
+ failure, do not retry, log the code".
8
+
9
+ Reaction columns: **Retry** says whether the same request may be sent again (for a write, always
10
+ with the **same** `Idempotency-Key`); **Show** says what the end user should see.
11
+
12
+ ## Request and auth
13
+
14
+ | Code | Status | Meaning | Retry | Show |
15
+ |---|---|---|---|---|
16
+ | `invalid_request` | 400 (405 for a wrong method) | malformed request: bad query, `limit` or `cursor`; missing or malformed `Idempotency-Key`; a body that is not an object of strings; a malformed pairing code or device id; a write called with the other method (`Allow` names the right one) | no; fix the code | nothing; a developer bug |
17
+ | `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
18
+ | `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
19
+ | `not_found` | 404 | no such route; an update whose id matches no record; a write naming a record that does not exist or belongs to another one | no | "record not found" when it is a record; otherwise a bug |
20
+ | `rate_limited` | 429 | too many failed pairing attempts with this key (5 per 60 s) | yes, after up to a minute (`Retry-After` when present) | "try again in a minute" |
21
+
22
+ Codes your `/v1` code never receives, listed so the enum is complete: `organization_required`
23
+ (403), `forbidden` (403), `conflict` (409), `already_member` (409) and `client_outdated` (400) are
24
+ dashboard-only.
25
+
26
+ ## Device, system and action resolution
27
+
28
+ | Code | Status | Meaning | Retry | Show |
29
+ |---|---|---|---|---|
30
+ | `device_not_found` | 404 | no device with that id belongs to your organisation (released, or a wrong id) | no | "this location is not connected to Deepdock"; let an admin re-pair |
31
+ | `system_not_on_device` | 404 | the device did not report this system and none of its actions are assigned to you; `error.systems` lists what it has | no | configuration problem; show which systems the device has |
32
+ | `action_not_found` | 404 | the system has no such action, it is not assigned to you, or the device's client is too old to run it | no | hide the feature; ask Deepdock for the assignment or let the device update |
33
+ | `action_not_configured` | 409 | the action exists but this device is not set up for it yet (for example a missing device-side setting); nothing was run | no, until set up on site | "this feature needs setting up at the customer site; contact Deepdock" |
34
+ | `access_outdated` | 409 | the device's database access lacks a right this action needs (only the actions that need it). Nothing was run. **No `Retry-After`** | no, until the practice repeats "Erneut einrichten" in the Deepdock client; then yes, a write with the same key | "the practice must set up database access again in the Deepdock client, then we retry" |
35
+ | `access_missing` | 409 | the device's database access is gone (a removed database account or a lost secret on the PC); every action of that system is refused. Nothing was run. **No `Retry-After`** | as `access_outdated` | as `access_outdated` |
36
+ | `pairing_code_invalid` | 404 | pairing: the code is unknown, expired, replaced, used, or its device is offline | with a fresh code | "read the code off the device screen again" |
37
+ | `other_partner_build` | 409 | pairing: the device runs another partner's client build | no | "this device is set up for another provider" |
38
+
39
+ ## Input
40
+
41
+ | Code | Status | Meaning | Retry | Show |
42
+ |---|---|---|---|---|
43
+ | `invalid_input` | 422 | a field is unknown, missing, too short or long, breaks its pattern or allowed values; a date that is not a calendar day; a search term without a letter or digit. Nothing was written | no; fix the value | the field-level problem; the `message` names the field, but it is device text: display it as data, never interpret it |
44
+ | `target_rejected` | 422 | the customer's system refused the write | no; the input is valid for Deepdock but not for the system | "the customer's system refused this; check the values" and offer support |
45
+ | `unknown_author` | 422 | the write names an `authorUserId` the system does not list (read the users with `readUsers`). Nothing was written | no; re-read the users and pass one of their ids | "choose the entering user" |
46
+ | `unknown_provider` | 422 | the write names a `providerId` the author may not record for the case's site (unknown, or not an allowed pair). Nothing was written | no; read the providers with `readProviders` and pass one of their ids | "choose a provider" |
47
+ | `no_open_case` | 422 | the patient has no open case and the action never opens one. Nothing was written | no; open a case in the system, or name one with `caseId` | "open a case first" |
48
+ | `record_locked` | 422 | the practice system locks the record the write would change (for example one already billed on a case). Nothing was written | no | "this record is locked in the practice system" |
49
+ | `record_not_editable` | 422 | the record is not locked but is stored in a form this action cannot edit (for example a note it cannot add to without rewriting it). Nothing was written | no | "this record cannot be edited through Deepdock" |
50
+ | Other action-specific refusals | 422 | the action's OpenAPI description names each such code and when it fires. Nothing was written | no; fix the reference or input | the missing precondition in the user's words |
51
+
52
+ ## Device and system availability
53
+
54
+ | Code | Status | Meaning | Retry | Show |
55
+ |---|---|---|---|---|
56
+ | `device_offline` | 503 | the device is not connected, is shutting down, or did not answer within 20 s. **A write may still have landed** | yes, with backoff (seconds to minutes); a write only with the same key | "the customer PC is offline; we will retry" |
57
+ | `action_paused` | 503 | the action is paused on the device (`GET .../systems` shows `reason`); only Deepdock resumes it | later, when `GET .../systems` shows it active; do not loop | "this feature is paused while Deepdock checks the customer system" |
58
+ | `system_unavailable` | 503 | the customer's program is stopped or missing on the device | later; often resolves when the customer restarts the program | "the customer's software is not running" |
59
+ | `target_busy` | 503 | the customer's system is busy or unreachable | yes, with backoff | "the customer's system is busy; retrying" |
60
+ | `temporarily_unavailable` | 503 | Deepdock could not check the key | yes, with backoff | "temporary problem; retrying" |
61
+
62
+ ## Write outcomes
63
+
64
+ | Code | Status | Meaning | Retry | Show |
65
+ |---|---|---|---|---|
66
+ | `check_failed` | 409 | the write's result did not match what the action expects, so nothing was saved; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
67
+ | `idempotency_conflict` | 409 | the key was used with a different input, its write is still running, or its outcome is unknown. One code, three causes; `message` must not be parsed to tell them apart | retry with the **same key and the same input** a bounded number of times with backoff (a running write finishes within seconds); if it still conflicts, handle it like `outcome_unknown`: read back, then decide | "a previous attempt is still in progress" |
68
+ | `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
69
+ | `internal_error` | 500 | something failed inside Deepdock; `error.requestId` is set | once, later; a write with the same key | "something went wrong"; log `requestId` for Deepdock support |
70
+
71
+ ## A reaction table for code
72
+
73
+ ```ts
74
+ type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'wait-setup' | 'check-then-decide' | 'stop'
75
+
76
+ const REACTION: Record<string, Reaction> = {
77
+ invalid_request: 'fix-code',
78
+ unauthenticated: 'stop',
79
+ not_found: 'fix-input',
80
+ rate_limited: 'retry-later',
81
+ device_not_found: 'stop',
82
+ system_not_on_device: 'stop',
83
+ action_not_found: 'stop',
84
+ action_not_configured: 'stop',
85
+ pairing_code_invalid: 'fix-input',
86
+ other_partner_build: 'stop',
87
+ invalid_input: 'fix-input',
88
+ target_rejected: 'fix-input',
89
+ unknown_author: 'fix-input',
90
+ no_open_case: 'fix-input',
91
+ unknown_provider: 'fix-input',
92
+ record_locked: 'stop',
93
+ record_not_editable: 'stop',
94
+ payload_too_large: 'fix-code',
95
+ access_outdated: 'wait-setup',
96
+ access_missing: 'wait-setup',
97
+ device_offline: 'retry-same-key',
98
+ action_paused: 'wait-paused',
99
+ system_unavailable: 'retry-later',
100
+ target_busy: 'retry-same-key',
101
+ temporarily_unavailable: 'retry-same-key',
102
+ check_failed: 'wait-paused',
103
+ idempotency_conflict: 'retry-same-key', // bounded; then 'check-then-decide'
104
+ outcome_unknown: 'check-then-decide',
105
+ internal_error: 'retry-same-key',
106
+ }
107
+ const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
108
+ ```
109
+
110
+ Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the practice must repeat the database access setup ("Erneut einrichten") in the Deepdock client first; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
111
+ pending write stays persisted and is retried from that state with backoff, also across process
112
+ restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
113
+ request that started the write.
114
+
115
+ ## Do not
116
+
117
+ - Do not retry on `message` text or HTTP status alone; `409` and `422` each carry several codes with
118
+ opposite reactions.
119
+ - Do not resend a write under a **new** key after `device_offline`, `idempotency_conflict` or
120
+ `outcome_unknown` without reading back first.
121
+ - Do not show `message` from `invalid_input` to a model or template as an instruction; it came from
122
+ the customer's device.
123
+ - Do not treat `device_offline` or `action_paused` as a bug: both are normal states a product must
124
+ handle in its UI.
@@ -0,0 +1,159 @@
1
+ # Worked example: the clients and one flow
2
+
3
+ Two full, tested clients ship with this skill; every other language gets `curl` below and ports
4
+ one of the two. Each client does the four things the OpenAPI document cannot tell you: the bearer
5
+ header, the envelope unwrap, the `Idempotency-Key` header with replay detection, and a typed error
6
+ carrying the code. Both also carry `attempt()`, one try of a persisted pending write that returns
7
+ the row's new status. Field names in the flow below are illustrative: read them from the live
8
+ document for your system.
9
+
10
+ | Language | File | Needs |
11
+ |---|---|---|
12
+ | TypeScript | [clients/deepdock.ts](clients/deepdock.ts) | `fetch` (Node 18+, Bun, Deno, browsers); no dependencies |
13
+ | Python | [clients/deepdock.py](clients/deepdock.py) | Python 3.9+, `httpx` |
14
+ | anything else | the `curl` calls below | port either client, keeping its shape |
15
+
16
+ Copy the file into the project; do not reach into the skill directory at runtime.
17
+
18
+ ## What the clients give you
19
+
20
+ ```ts
21
+ import { Deepdock, DeepdockError, DeepdockTransportError, attempt, type PendingWrite } from './deepdock'
22
+
23
+ const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // 30 s timeout by default
24
+ const page = await dd.read<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' }, { limit: 50 })
25
+ for await (const p of dd.readAll<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' })) { /* every page */ }
26
+ const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', 'POST', key, input)
27
+ const systems = await dd.systems(deviceId) // live; can throw device_offline
28
+ const device = await dd.pair('K7Q-4MX', 'Demo') // keep device.id
29
+ ```
30
+
31
+ ```python
32
+ from deepdock import Deepdock, DeepdockError, DeepdockTransportError, PendingWrite, attempt
33
+
34
+ dd = Deepdock(os.environ["DEEPDOCK_API_KEY"])
35
+ page = dd.read(device_id, "example-erp", "searchNotes", {"text": "TEST-DEEPDOCK-"}, limit=50)
36
+ res = dd.write(device_id, "example-erp", "createNote", "POST", key, body) # res.data, res.replayed
37
+ ```
38
+
39
+ Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on `code`
40
+ ([errors.md](errors.md)). `DeepdockTransportError` means no answer or not Deepdock's envelope
41
+ (timeout, network, a proxy page): for a write, retry with the same key.
42
+
43
+ ## Sending a write: persist first, then one attempt at a time
44
+
45
+ Persist the pending write (key, device, action, method, input, status) **before** the first send,
46
+ so a crash between send and answer cannot produce a second record under a new key. `attempt()`
47
+ moves the row to `done`, `sent` (retry later, same key), `unknown` (read back before anything
48
+ else) or `refused` (nothing was written; a changed input gets a new key). A job retries `sent`
49
+ rows with backoff: seconds, then minutes, for hours if the device stays offline.
50
+
51
+ ```ts
52
+ const row: PendingWrite = {
53
+ key: `order-${order.id}-create-note`, // 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event
54
+ deviceId, system: 'example-erp', action: 'createNote', method: 'POST',
55
+ input, status: 'pending', conflictRetries: 0,
56
+ }
57
+ await store.save(row) // before the first send
58
+ const next = await attempt(dd, row) // one try
59
+ await store.save(next) // the job picks up 'sent' rows later
60
+ ```
61
+
62
+ ## One flow: create a record on the demo device and read it back
63
+
64
+ ```ts
65
+ import { randomUUID } from 'node:crypto'
66
+ import { Deepdock, attempt, type PendingWrite } from './deepdock'
67
+
68
+ const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!)
69
+ const deviceId = process.env.DEEPDOCK_TEST_DEVICE! // the demo device the human named
70
+ const system = 'example-erp'
71
+ type Note = { noteId: number; text: string }
72
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
73
+
74
+ export const run = async (store: { save: (row: PendingWrite) => Promise<void> }) => {
75
+ const marker = `TEST-DEEPDOCK-${randomUUID().replaceAll('-', '').slice(0, 6).toUpperCase()}`
76
+ let row: PendingWrite = {
77
+ key: `demo-${marker}-create`, deviceId, system, action: 'createNote', method: 'POST',
78
+ input: { text: marker, orderNumber: '1001' },
79
+ status: 'pending', conflictRetries: 0,
80
+ }
81
+ await store.save(row)
82
+
83
+ // The demo retries inline a few times; a product retries from the store in a job.
84
+ for (let delay = 2_000; row.status === 'pending' || row.status === 'sent'; delay = Math.min(delay * 2, 60_000)) {
85
+ row = await attempt(dd, row)
86
+ await store.save(row)
87
+ if (row.status === 'sent') await sleep(delay)
88
+ }
89
+ if (row.status === 'refused') throw new Error(`refused: ${row.refusedCode}`)
90
+
91
+ if (row.status === 'unknown') {
92
+ // Read back before deciding: the write may have landed.
93
+ const page = await dd.read<Note>(deviceId, system, 'searchNotes', { text: marker })
94
+ const landed = page.items.find((p) => p.text === marker)
95
+ if (!landed) throw new Error('write did not land; resend under a NEW key from the store')
96
+ row = { ...row, status: 'done', result: { noteId: landed.noteId } }
97
+ await store.save(row)
98
+ }
99
+
100
+ const created = row.result as { noteId: number }
101
+ const page = await dd.read<Note>(deviceId, system, 'searchNotes', { text: marker })
102
+ if (!page.items.some((p) => p.noteId === created.noteId)) throw new Error('created record not found by search')
103
+
104
+ // Cleanup list for the PR: record id, device, time.
105
+ console.log(`CREATED ${system} noteId=${created.noteId} on device ${deviceId} at ${new Date().toISOString()}`)
106
+ }
107
+ ```
108
+
109
+ ## curl, for any other language
110
+
111
+ ```bash
112
+ BASE=https://api.deepdock.ai
113
+ AUTH="Authorization: Bearer $DEEPDOCK_API_KEY"
114
+
115
+ # Pair a device (keep data.id)
116
+ curl -sS -X POST "$BASE/v1/devices" -H "$AUTH" -H 'Content-Type: application/json' \
117
+ -d '{"code":"K7Q-4MX","label":"Demo"}'
118
+
119
+ # The device's systems and action states (live; can answer device_offline)
120
+ curl -sS "$BASE/v1/devices/$DEVICE/systems" -H "$AUTH"
121
+
122
+ # The action index (your assigned actions, with schemas)
123
+ curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" -H "$AUTH"
124
+
125
+ # A read, paged: pass data.nextCursor back as cursor
126
+ curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes?text=TEST-DEEPDOCK-&limit=50" -H "$AUTH"
127
+
128
+ # A create: new Idempotency-Key per intended write, the same on every retry; 201, or 200 with
129
+ # Idempotent-Replayed: true on a replay (-i shows the headers)
130
+ curl -sS -i -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
131
+ -H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-create" -H 'Content-Type: application/json' \
132
+ -d '{"text":"TEST-DEEPDOCK-K3M9QA","orderNumber":"1001"}'
133
+
134
+ # An update: PATCH, same rules
135
+ curl -sS -i -X PATCH "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/updateNote" \
136
+ -H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-update" -H 'Content-Type: application/json' \
137
+ -d '{"noteId":"1001","text":"TEST-DEEPDOCK-K3M9QA edited"}'
138
+
139
+ # Release a device
140
+ curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
141
+ ```
142
+
143
+ A port in another language keeps the clients' shape: one `send` that sets the bearer header,
144
+ parses the envelope, throws a typed error with `code` on `{ error }` and a transport error on no
145
+ answer or non-JSON; `read` with `limit` and `cursor`; `write` with `Idempotency-Key` and the
146
+ `Idempotent-Replayed` header; `attempt` over a persisted pending write.
147
+
148
+ ## Mapping the user's intent to a call (checklist)
149
+
150
+ 1. Which device? From your product's configuration for that customer site; never from the
151
+ user's free text.
152
+ 2. Is the action active? `systems()` once per session; show paused features as paused.
153
+ 3. Which action and method? From the stored index for that system.
154
+ 4. Does the write need a reference (author user, open case, record id)? Read it first.
155
+ 5. Build the input as strings; validate against the schema (`pattern`, `minLength`, `maxLength`,
156
+ `enum`) before sending so the user sees the rule where they typed.
157
+ 6. Choose the idempotency key from your business event and persist it with the pending write.
158
+ 7. Send; map the code with the reaction table; show the user the right sentence.
159
+ 8. Store the returned id as the system gave it.
@@ -0,0 +1,97 @@
1
+ # Idempotency for writes
2
+
3
+ Every `POST` or `PATCH` to an action carries an `Idempotency-Key` header. Without it the request is
4
+ refused with `400 invalid_request`.
5
+
6
+ ## The key
7
+
8
+ - 8 to 128 characters from `A-Z a-z 0-9 _ -`.
9
+ - **One key per intended write.** Derive it from your own business event (an order id, a form
10
+ submission id, a job id plus the action name), or generate a UUID and persist it with the pending
11
+ write before sending. The key must survive a crash of your process, or a restart will write twice.
12
+ - **The same key for every retry of that write.** Never regenerate it on retry.
13
+ - Keys are remembered **per device**. The same key on another device is a different write.
14
+ - A successful key is remembered for at least 7 days.
15
+ - A write that was refused before anything was written (`invalid_input`, `target_rejected`, an
16
+ action-specific 422 such as `unknown_author`, `not_found`) or not saved (`check_failed`) frees its
17
+ key: you may retry with the same key. After changing the input, use a new key; the old one may
18
+ still be remembered with the old input. `idempotency_conflict` does **not** free a key.
19
+
20
+ ## What a repeat answers
21
+
22
+ | Situation | Answer |
23
+ |---|---|
24
+ | same key, same input, earlier write succeeded | `200` with the original `data` and `Idempotent-Replayed: true`; nothing written again |
25
+ | same key, different input | `409 idempotency_conflict` |
26
+ | same key, the earlier write is still running | `409 idempotency_conflict`; wait, then retry with the same key and input |
27
+ | same key, the earlier write's outcome is unknown | `409 idempotency_conflict`; the key stays blocked |
28
+
29
+ The three `idempotency_conflict` causes share one code and `message` is not to be parsed. One rule
30
+ covers all three: retry with the same key and the same input a bounded number of times with
31
+ backoff (a running write finishes within seconds); if it still conflicts, treat it as
32
+ `outcome_unknown` below.
33
+
34
+ Treat a replayed `200` as success. Read the header if you need to know it was a replay, for
35
+ example to avoid double-logging.
36
+
37
+ ## The three outcomes you must plan for
38
+
39
+ ### `503 device_offline` on a write
40
+
41
+ The device did not answer within 20 seconds, or was not connected. **The write may still have
42
+ landed.** Retry with the **same key**: you get either the stored result (replayed) or a fresh run,
43
+ never two writes. Back off between retries (seconds, then minutes); a device can be off for hours,
44
+ so the retry runs from the persisted pending write in a job or queue, not inside the request that
45
+ started it. A client-side timeout or a network failure on a write is the same case: same key.
46
+
47
+ ### `500 outcome_unknown`
48
+
49
+ The customer's system did not confirm the write. Deepdock cannot tell whether it landed. The key is
50
+ blocked: any reuse answers `409 idempotency_conflict`.
51
+
52
+ Do not write again under a new key until you know. Read the record back with a read action (for a
53
+ create, search by the value you wrote; for an update, read the record by id) and decide:
54
+
55
+ - found with the written values: treat as done, record it, move on;
56
+ - not found: write again under a **new** key;
57
+ - cannot tell (the read action does not expose the field): stop and hand the case to a human with
58
+ the record identifiers.
59
+
60
+ ### `409 check_failed`
61
+
62
+ The write's result did not match, so nothing was saved, and the action is now paused. Do not retry;
63
+ the next calls answer `503 action_paused` until Deepdock resumes it. Tell the user the change was not
64
+ saved.
65
+
66
+ ## Persisting state
67
+
68
+ A robust integration keeps, per intended write: the idempotency key, the device id, the action, the
69
+ input, a status (`pending`, `sent`, `done`, `unknown`, `refused`) and the result. Transitions:
70
+
71
+ ```
72
+ pending ──send──▶ sent ──2xx (fresh or replayed)──────────────▶ done
73
+ │ ──device_offline, timeout, network error, target_busy, temporarily_unavailable, internal_error ▶ sent (retry later, same key)
74
+ │ ──idempotency_conflict, retries left ───▶ sent (retry, same key, same input)
75
+ │ ──idempotency_conflict, retries spent ──▶ unknown (read back before anything else)
76
+ │ ──outcome_unknown ──────────────────────▶ unknown (read back before anything else)
77
+ │ ──invalid_input, target_rejected, action-specific 422, not_found, check_failed ▶ refused (key is free; new input gets a new key)
78
+ │ ──action_paused, system_unavailable ────▶ sent (retry once the action is active again)
79
+ ```
80
+
81
+ ## Example
82
+
83
+ ```ts
84
+ const key = `order-${order.id}-create-note` // stable per business event, 8 to 128 chars
85
+
86
+ const res = await fetch(url, {
87
+ method: 'POST',
88
+ headers: {
89
+ Authorization: `Bearer ${apiKey}`,
90
+ 'Idempotency-Key': key,
91
+ 'Content-Type': 'application/json',
92
+ },
93
+ body: JSON.stringify(input),
94
+ signal: AbortSignal.timeout(30_000),
95
+ })
96
+ const replayed = res.headers.get('Idempotent-Replayed') === 'true'
97
+ ```
@@ -0,0 +1,80 @@
1
+ # Testing safely
2
+
3
+ Actions reach a real installation of the customer's system: the device you name is the one that
4
+ gets written to. These rules are not optional.
5
+
6
+ ## The test device
7
+
8
+ - Test against **a demo device of your own**: a Windows machine with the same system installed as
9
+ your customers run, with the Deepdock client paired to your organisation, and invented, marked
10
+ data on it.
11
+ - **Never run a write against a device the human has not named as the test device.** If no device
12
+ has been named, ask. Do not pick the first device from `devices list`; it may be a live customer.
13
+ - Pin the demo device's UUID in an environment variable (`DEEPDOCK_TEST_DEVICE`) and refuse to run
14
+ tests without it.
15
+
16
+ ## Test data
17
+
18
+ - Invented values only: no real customer, employee or address data, even "just one row",
19
+ even from a screenshot. A fixture is copied into every clone and CI log for ever.
20
+ - Every record you create carries the marker **`TEST-DEEPDOCK-<6 random characters>`** in its
21
+ name-like field (surname, company name), so it can be found and removed. Keep the marker through
22
+ updates: when you test a name update, write a second `TEST-DEEPDOCK-...` value.
23
+ - Other fields take short values valid for their schema rule: a real past date, the first enum
24
+ member, `1001` for an order number, `Beispielstadt` for a city.
25
+ - Keep the system's own text rules in mind: the OpenAPI document's field patterns and descriptions
26
+ say what a text field accepts; stick to plain Latin letters in test values.
27
+
28
+ ## Cleanup
29
+
30
+ No action deletes anything, and nothing is undone through the API. So:
31
+
32
+ 1. Every test run prints what it created: record id, device, time, and how many change-log entries
33
+ the updates added. The CLI runs no action, so your own test prints this.
34
+ 2. Paste the list into the PR or the task that created it, with an owner and a date by which it is
35
+ removed.
36
+ 3. The partner removes the records through the system's own user interface on the demo machine, or
37
+ resets the machine from a snapshot. Change-log entries stay until the reset.
38
+
39
+ ## The loop your test runs
40
+
41
+ The CLI runs no action, so this loop is your code's, against the demo device:
42
+
43
+ 1. `deepdock devices systems <device> --org <slug> --json`: the system is running and every action
44
+ you need is active; otherwise stop and read the pause reason and `deepdock incidents list`.
45
+ 2. **Create** with a fresh idempotency key and `TEST-DEEPDOCK-` names: expect `201` and an id.
46
+ 3. **Replay** the same request with the same key: expect `200`, `Idempotent-Replayed: true`, no
47
+ second record.
48
+ 4. **Update** each update action once against that id, a new key each, changed values of the same
49
+ shape (a name update writes a second `TEST-DEEPDOCK-` name).
50
+ 5. **Read back** with the last written name as the search term: expect the id with the updated
51
+ values.
52
+ 6. Print the cleanup list.
53
+
54
+ The proof is your client; the CLI only shows what the device offers.
55
+
56
+ ## Keys during development
57
+
58
+ - Run `deepdock keys create "<project> dev" --expires 30d --write-env .env.local --org <slug>`.
59
+ A human approves it in the browser; the CLI writes the key into the file and keeps no copy.
60
+ Neither you nor the human sees the value; you work with its id, prefix and file name only.
61
+ - Never ask for, read, print, echo, log or commit a key value. Use the key only against the demo
62
+ device.
63
+ - It reaches every device of the organisation until it expires or is revoked. Tell the human to
64
+ revoke it when the integration ships and to create the live key with the same flow from their
65
+ own CLI.
66
+
67
+ ## Logs and prompts
68
+
69
+ - Never log an action's `input` or a read's `items` in your running product: they are the customer's
70
+ records. Log the device id, the action, the code and the request id.
71
+ - Never paste real records into an agent prompt or a ticket. Use the `TEST-DEEPDOCK-` records from
72
+ the demo device when you need a concrete example.
73
+
74
+ ## Unit tests of your client
75
+
76
+ Mock the HTTP layer with the shapes from the OpenAPI document and this skill: `{ data }`,
77
+ `{ error: { code } }`, `Idempotent-Replayed: true`, a `503 device_offline` on a write followed by
78
+ a replayed `200`, a `500 outcome_unknown` followed by a read back. Cover every row of
79
+ [errors.md](errors.md) your product reacts to. Do not record real responses from a customer
80
+ device into fixtures.