{
  "file": "F28-canvas-extensions.md",
  "bullet": "TypeScript client (`F28.ts-client`).",
  "ids": [
    "F28.ts-client"
  ],
  "line": 671,
  "script": "TX=$(control-agent-server fixture canvas-app --name qa-f28-ts --backend ok --print-path)\ncontrol-agent-server api POST /api/canvas-extensions/install --json \"{\\\"source\\\": \\\"$TX\\\"}\" --expect 200 --quiet\ncat > \"$F28FX/ts_canvas.mjs\" <<'JS'\nimport assert from 'node:assert/strict';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nconst { CanvasExtensionsClient } = await import(pathToFileURL(resolve('clients/typescript/dist/clients.js')).href);\nconst options = { host: process.env.AGENT_SERVER_URL, apiKey: process.env.SESSION_API_KEY };\nconst client = new CanvasExtensionsClient(options);\nconst httpError = (status, detail) => (err) =>\n  err.name === 'HttpError' && err.status === status && (detail === undefined || err.detail === detail);\nconst app = 'qa-f28-ts';\nlet status = await client.getBackendStatus(app);\nassert.equal(status.state, 'stopped');\nassert.equal(status.prepared_revision, null);\nconst revision = status.revision;\nawait assert.rejects(client.startBackend(app, revision), httpError(409, 'backend revision must be prepared before start'));\nawait assert.rejects(client.prepareBackend(app, 'qa-wrong'),\n  httpError(409, 'approval revision does not match installed revision'));\nassert.equal((await client.prepareBackend(app, revision)).prepared_revision, revision);\nstatus = await client.startBackend(app, revision);\nassert.equal(status.state, 'ready');\nassert.ok(status.pid > 0 && status.port > 0);\nconst pid = status.pid;\nassert.equal((await client.getBackendStatus(app)).pid, pid);\nconst logs = await client.getBackendLogs(app);\nassert.equal(logs.name, app);\nassert.equal(typeof logs.logs, 'string');\nawait assert.rejects(client.deleteBackendData(app), httpError(409, 'stop the backend before deleting its data'));\nstatus = await client.stopBackend(app);\nassert.equal(status.state, 'stopped');\nassert.equal(status.pid, null);\nassert.equal((await client.deleteBackendData(app)).state, 'stopped');\nawait assert.rejects(client.getBackendStatus('Bad_Name'), httpError(422));\nassert.equal((await client.getBackendStatus('qa-ghost')).state, 'missing');\n\nconst slow = 'qa-f28-logs';\nconst slowRevision = (await client.getBackendStatus(slow)).revision;\nassert.equal((await client.prepareBackend(slow, slowRevision)).prepared_revision, slowRevision);\nconst impatient = new CanvasExtensionsClient({ ...options, timeout: 2000 });\nconst started = Date.now();\nawait assert.rejects(impatient.startBackend(slow, slowRevision),\n  (err) => err.name !== 'HttpError' && err.message === 'Request timeout after 2000ms');\nassert.ok(Date.now() - started < 10000);\nstatus = await client.getBackendStatus(slow);\nassert.equal(status.state, 'starting');\nassert.ok(status.pid > 0);\nlet slowLogs;\nfor (let attempt = 0; attempt < 50; attempt += 1) {\n  slowLogs = await client.getBackendLogs(slow);\n  if (slowLogs.logs.includes('[stderr] qa-f28-err') && slowLogs.logs.includes('[stdout] qa-f28-out')) break;\n  await new Promise((done) => setTimeout(done, 200));\n}\nassert.ok(slowLogs.logs.includes('[stdout] qa-f28-out') && slowLogs.logs.includes('[stderr] qa-f28-err'));\nconst tail = await client.getBackendLogs(slow, 8);\nassert.equal(tail.logs.length, 8);\nassert.equal(tail.truncated, true);\nassert.equal((await client.getBackendStatus(slow)).state, 'starting');\nassert.equal((await client.stopBackend(slow)).state, 'stopped');\nclient.close();\nimpatient.close();\nconsole.log(`QA_F28_TS_OK ready_pid=${pid} starting_pid=${status.pid}`);\nJS\ntest -f clients/typescript/dist/clients.js || (cd clients/typescript && npm ci && npm run build)\ncontrol-agent-server exec --timeout 120 --expect-output QA_F28_TS_OK --save F28.ts-client/program -- node \"$F28FX/ts_canvas.mjs\"\ncontrol-agent-server api GET /api/canvas-extensions/installed/qa-f28-logs/backend --check state eq stopped --check pid eq null\ncontrol-agent-server api GET /api/canvas-extensions/installed/qa-f28-ts/backend --check state eq stopped \\\n  --check prepared_revision exists --check pid eq null\nif pgrep -f \"$BK/artifacts/qa-f28-logs/\"; then false; fi\nif pgrep -f \"$BK/artifacts/qa-f28-ts/\"; then false; fi\ntest ! -e \"$BK/data/qa-f28-ts\"\ncontrol-agent-server api DELETE /api/canvas-extensions/installed/qa-f28-ts --expect 200 --quiet\n",
  "exit_code": 0,
  "elapsed_ms": 12261,
  "failed_line": null,
  "failed_command": null,
  "stdout": "{\n  \"ok\": true,\n  \"status\": 200,\n  \"request\": {\n    \"method\": \"POST\",\n    \"path\": \"/api/canvas-extensions/install\",\n    \"query\": [],\n    \"auth\": \"header\",\n    \"files\": []\n  },\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"content-length\": \"1228\",\n      \"content-type\": \"application/json\"\n    },\n    \"truncated\": false\n  },\n  \"elapsed_ms\": 329\n}\n{\n  \"ok\": true,\n  \"exit_code\": 0,\n  \"elapsed_ms\": 3605,\n  \"stdout_tail\": \"QA_F28_TS_OK ready_pid=41087 starting_pid=41103\\n\",\n  \"stderr_tail\": \"\",\n  \"saved\": \"/workspace/verification-artifacts/runs/20261013-024207-5ac2-map-f28/evidence/F28.ts-client/program.json\"\n}\n{\n  \"ok\": true,\n  \"status\": 200,\n  \"request\": {\n    \"method\": \"GET\",\n    \"path\": \"/api/canvas-extensions/installed/qa-f28-logs/backend\",\n    \"query\": [],\n    \"auth\": \"header\",\n    \"body\": null,\n    \"files\": []\n  },\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"content-length\": \"256\",\n      \"content-type\": \"application/json\"\n    },\n    \"body\": {\n      \"name\": \"qa-f28-logs\",\n      \"state\": \"stopped\",\n      \"revision\": \"sha256:044731adb5f55a84697cf9c972cc6acd72d91c7ea86094e0b1489d85f35d9698\",\n      \"prepared_revision\": \"sha256:044731adb5f55a84697cf9c972cc6acd72d91c7ea86094e0b1489d85f35d9698\",\n      \"pid\": null,\n      \"port\": null,\n      \"detail\": null\n    },\n    \"truncated\": false\n  },\n  \"elapsed_ms\": 268,\n  \"checks\": [\n    {\n      \"check\": \"state eq stopped\",\n      \"ok\": true,\n      \"actual\": \"stopped\"\n    },\n    {\n      \"check\": \"pid eq null\",\n      \"ok\": true,\n      \"actual\": \"null\"\n    }\n  ]\n}\n{\n  \"ok\": true,\n  \"status\": 200,\n  \"request\": {\n    \"method\": \"GET\",\n    \"path\": \"/api/canvas-extensions/installed/qa-f28-ts/backend\",\n    \"query\": [],\n    \"auth\": \"header\",\n    \"body\": null,\n    \"files\": []\n  },\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"content-length\": \"254\",\n      \"content-type\": \"application/json\"\n    },\n    \"body\": {\n      \"name\": \"qa-f28-ts\",\n      \"state\": \"stopped\",\n      \"revision\": \"sha256:2cdeb167bfe089a5f617b4fe98d3758654456ba5b3b1f5d0e3da1b556bfd40eb\",\n      \"prepared_revision\": \"sha256:2cdeb167bfe089a5f617b4fe98d3758654456ba5b3b1f5d0e3da1b556bfd40eb\",\n      \"pid\": null,\n      \"port\": null,\n      \"detail\": null\n    },\n    \"truncated\": false\n  },\n  \"elapsed_ms\": 135,\n  \"checks\": [\n    {\n      \"check\": \"state eq stopped\",\n      \"ok\": true,\n      \"actual\": \"stopped\"\n    },\n    {\n      \"check\": \"prepared_revision exists\",\n      \"ok\": true,\n      \"actual\": \"sha256:2cdeb167bfe089a5f617b4fe98d3758654456ba5b3b1f5d0e3da1b556bfd40eb\"\n    },\n    {\n      \"check\": \"pid eq null\",\n      \"ok\": true,\n      \"actual\": \"null\"\n    }\n  ]\n}\n{\n  \"ok\": true,\n  \"status\": 200,\n  \"request\": {\n    \"method\": \"DELETE\",\n    \"path\": \"/api/canvas-extensions/installed/qa-f28-ts\",\n    \"query\": [],\n    \"auth\": \"header\",\n    \"files\": []\n  },\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"content-length\": \"54\",\n      \"content-type\": \"application/json\"\n    },\n    \"truncated\": false\n  },\n  \"elapsed_ms\": 280\n}\n",
  "stderr": ""
}
