[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69A74M9JKAMFHPTSZ38S50K":3,"skill-same:01M69A74M9JKAMFHPTSZ38S50K":73},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":45,"linkOnly":46,"markdownContent":47,"markdownBody":48,"frontMatter":49,"refName":50,"commitSha":51,"skillDirectory":52,"forksCount":53,"repositoryHtmlUrl":54,"repositoryHomepageUrl":6,"ownerHtmlUrl":55,"ownerBlogUrl":56,"ownerDescription":57,"updatedAt":58,"install":59,"resources":72},"01M69A74M9JKAMFHPTSZ38S50K","azure-ps-test-runner-vm",null,"Create a throwaway Azure VM next to the HorizonDB server and run the full `.\u002Fscripts\u002Frun-tests.sh --all-providers` suite on it, with credentials copied for one run only. Use when HorizonDB tests time out from a laptop far from the server, when GitHub runners are slow or unavailable, or when asked to set up, use or tear down a PilotSwarm test runner VM.","microsoft\u002FPilotSwarm","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69A6RNK492A5JSNBTXANKX5",16,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fazure-ps-test-runner-vm\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44],"test","test runner","azure ps test runner vm","azure","vm","PS","runner","when","github","throwaway","down","for","with","tests","time","up","only","use","SET","runners","create","2026-11-10 10:12:30",false,"---\nname: azure-ps-test-runner-vm\ndescription: Create a throwaway Azure VM next to the HorizonDB server and run the full `.\u002Fscripts\u002Frun-tests.sh --all-providers` suite on it, with credentials copied for one run only. Use when HorizonDB tests time out from a laptop far from the server, when GitHub runners are slow or unavailable, or when asked to set up, use or tear down a PilotSwarm test runner VM.\n---\n\n# Azure test runner VM\n\nA plain Ubuntu VM in the same Azure region as the HorizonDB server. It runs\nthe full all-providers suite from a clone of the repo. It is not a GitHub\nActions runner; for that, see the `Manage Azure CI runner` workflow and\n`deploy\u002Fproviders\u002Fazure\u002Fci\u002Frunner\u002FREADME.md`.\n\n## Why\n\nThe HorizonDB phase makes many short database calls in a row. Distance\ndecides whether it passes:\n\n| Where the tests run | One `SELECT 1` | Result |\n|---|---|---|\n| A laptop on another continent | 130–160 ms | Most HorizonDB SDK tests hit their time limits |\n| A VM in the server's region | 0–1 ms | The full suite passes |\n\nMeasure it first (step 6) when in doubt.\n\n## What you need\n\n- Azure CLI signed in, with rights to create a resource group and a VM.\n- The repo's local test config files: `.env`, `.env.horizondb` and\n  `.model_providers.json`. They hold credentials (a GitHub token, model keys,\n  the HorizonDB connection). See \"Credentials\" below before copying them.\n\nSet these once in your shell. Use your own values:\n\n```bash\nSUB=\u003Csubscription-id>\nRG=\u003Cresource-group>            # a new, dedicated group, e.g. pilotswarm-testrunner-rg\nVM=\u003Cvm-name>                   # e.g. ps-testrunner\nLOC=\u003Chorizondb-region>         # the region of your HorizonDB server\nKEY=~\u002F.ssh\u002Fid_ed25519_ps_testrunner\nMYIP=$(curl -s https:\u002F\u002Fapi.ipify.org)\n```\n\n## 1. SSH key and setup script\n\n```bash\n[ -f \"$KEY\" ] || ssh-keygen -q -t ed25519 -N '' -C 'pilotswarm test runner' -f \"$KEY\"\n\ncat > cloud-init.yaml \u003C\u003C'EOF'\n#cloud-config\npackage_update: true\npackages: [git, build-essential, python3, jq, ca-certificates, curl, docker.io, unzip]\nruncmd:\n  - curl -fsSL https:\u002F\u002Fdeb.nodesource.com\u002Fsetup_24.x | bash -\n  - apt-get install -y nodejs\n  - usermod -aG docker azureuser\n  - systemctl enable --now docker\nEOF\n```\n\n## 2. Create the VM\n\nUse 8 cores. The suite runs 8 test files at once, each with its own workers,\nplus PostgreSQL in Docker. On 4 cores the tests compete for CPU and hit their\ntime limits.\n\n```bash\naz group create --subscription \"$SUB\" -n \"$RG\" -l \"$LOC\" -o none\naz vm create --subscription \"$SUB\" -g \"$RG\" -n \"$VM\" \\\n  --image Canonical:ubuntu-24_04-lts:server:latest \\\n  --size Standard_D8s_v5 --os-disk-size-gb 128 --storage-sku Premium_LRS \\\n  --admin-username azureuser --ssh-key-values \"$KEY.pub\" --authentication-type ssh \\\n  --public-ip-sku Standard --nsg-rule NONE --custom-data cloud-init.yaml \\\n  --query publicIpAddress -o tsv\n```\n\nWrite down the IP it prints.\n\n## 3. SSH from your IP only, and a nightly shutdown\n\n```bash\nNSG=$(az network nsg list --subscription \"$SUB\" -g \"$RG\" --query '[0].name' -o tsv)\naz network nsg rule create --subscription \"$SUB\" -g \"$RG\" --nsg-name \"$NSG\" \\\n  -n ssh-from-me --priority 1000 --direction Inbound --access Allow --protocol Tcp \\\n  --source-address-prefixes \"$MYIP\u002F32\" --destination-port-ranges 22 -o none\naz vm auto-shutdown --subscription \"$SUB\" -g \"$RG\" -n \"$VM\" --time 0200 -o none   # UTC\n```\n\nAdd an alias to `~\u002F.ssh\u002Fconfig`:\n\n```\nHost ps-testrunner\n    HostName \u003Cvm-ip>\n    User azureuser\n    IdentityFile ~\u002F.ssh\u002Fid_ed25519_ps_testrunner\n    IdentitiesOnly yes\n    StrictHostKeyChecking accept-new\n    ServerAliveInterval 30\n```\n\nThen wait for the setup script: `ssh ps-testrunner 'cloud-init status --wait; node --version; docker --version'`.\n\n**If SSH times out** while the VM runs, check the security rules:\n`az network nsg rule list -g \"$RG\" --nsg-name \"$NSG\" -o table`. Some\norganizations add their own deny rule for port 22 from the internet, with a\nhigher priority than yours. Do not edit those rules. Connect from the network\nthey allow, or drive the VM with Run Command (see \"Without SSH\").\n\n## 4. The repo\n\n```bash\nssh ps-testrunner 'git clone -q https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm.git pilotswarm\n  cd pilotswarm && git checkout -q -B \u003Cbranch> origin\u002F\u003Cbranch> && npm ci --no-audit --no-fund'\n```\n\nTo test another commit later: `git fetch origin \u003Cbranch> && git checkout -B \u003Cbranch> origin\u002F\u003Cbranch> && npm ci`.\n\n## 5. PostgreSQL\n\nThe container's password must match `DATABASE_URL` in the `.env` you copy.\n`run-tests.sh` refuses to run with fewer than 1500 connections.\n\n```bash\nssh ps-testrunner 'docker run -d --name pilotswarm-pg --restart unless-stopped \\\n    -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=\u003Cpassword-from-DATABASE_URL> \\\n    -e POSTGRES_DB=pilotswarm -p 127.0.0.1:5432:5432 postgres:16 \\\n  && sleep 5 \\\n  && docker exec pilotswarm-pg psql -U postgres -qc \"ALTER SYSTEM SET max_connections = 1500;\" \\\n  && docker restart pilotswarm-pg'\n```\n\n## 6. Credentials: copy for one run, delete after\n\nThe three config files hold real credentials. Keep them on the VM only while a\nrun needs them:\n\n1. Copy them right before the run.\n2. In the same step, start a watcher that deletes them when the run ends.\n3. Never leave them between runs. Ask the person before copying them to a\n   machine that did not hold them before.\n\n**With SSH:**\n\n```bash\nscp .env .env.horizondb .model_providers.json ps-testrunner:pilotswarm\u002F\nssh ps-testrunner 'cd pilotswarm && chmod 600 .env .env.horizondb .model_providers.json\n  grep -q \"^HORIZON_DATABASE_URL=.\" .env.horizondb && echo \"hdb config: ok\"'\n```\n\nCheck the HorizonDB latency once:\n\n```bash\nssh ps-testrunner 'cd pilotswarm && node --env-file=.env.horizondb -e \"\nconst pg = require(\\\"pg\\\");\n(async () => {\n  const u = new URL(process.env.HORIZON_DATABASE_URL);\n  u.searchParams.delete(\\\"sslmode\\\"); u.searchParams.delete(\\\"uselibpqcompat\\\");\n  const c = new pg.Client({ connectionString: u.toString(), ssl: { rejectUnauthorized: false } });\n  await c.connect();\n  const t = []; for (let i = 0; i \u003C 5; i++) { const s = Date.now(); await c.query(\\\"SELECT 1\\\"); t.push(Date.now() - s); }\n  console.log(\\\"SELECT 1 ms:\\\", t.join(\\\", \\\")); await c.end();\n})().catch((e) => console.log(\\\"error:\\\", e.code || e.message));\"'\n```\n\n## 7. Run the suite\n\nStart it detached, so it keeps going when your laptop sleeps. Then start the\nwatcher that deletes the credentials when the run ends.\n`PS_TEST_SKIP_STALE_CLEANUP=1` skips the global sweep of old test schemas, so\nthe run cannot drop schemas another run on the same HorizonDB still uses.\n\n```bash\nssh ps-testrunner 'cd pilotswarm\n  PS_TEST_SKIP_STALE_CLEANUP=1 setsid nohup bash -c \".\u002Fscripts\u002Frun-tests.sh --all-providers > ~\u002Fall-providers.log 2>&1; echo EXIT=\\$? >> ~\u002Fall-providers.log\" > \u002Fdev\u002Fnull 2>&1 \u003C \u002Fdev\u002Fnull &\n  sleep 3\n  setsid nohup bash -c \"while pgrep -f \\\"[r]un-tests.sh --all-providers\\\" > \u002Fdev\u002Fnull; do sleep 30; done; shred -u ~\u002Fpilotswarm\u002F.env ~\u002Fpilotswarm\u002F.env.horizondb ~\u002Fpilotswarm\u002F.model_providers.json; echo \\\"credentials removed \\$(date -u)\\\" >> ~\u002Fsecrets-cleanup.log\" > \u002Fdev\u002Fnull 2>&1 \u003C \u002Fdev\u002Fnull &\n  echo started'\n```\n\nProgress and result:\n\n```bash\nssh ps-testrunner 'sed \"s\u002F\\x1b\\[[0-9;]*m\u002F\u002Fg\" ~\u002Fall-providers.log | grep -aE \"Provider phase|Test Files|Tests  |Combined provider result|EXIT=\"'\n```\n\nA full run takes about 35–40 minutes: the stock PostgreSQL pass (about 22),\nthen the HorizonDB phase. To run only some files on HorizonDB:\n`.\u002Fscripts\u002Frun-tests.sh --with-horizondb --suite=\u003Cname> --suite=\u003Cname>`.\n\n## Without SSH: Run Command\n\nUse `az vm run-command invoke` for commands that carry no secret (status,\nlogs, pkill). It returns the output, but Azure keeps the script, so **never\nput credentials in it**.\n\nTo copy the credential files, use Run Command v2 with protected parameters.\nAzure stores them encrypted and never returns them:\n\n```bash\naz vm run-command create --subscription \"$SUB\" -g \"$RG\" --vm-name \"$VM\" \\\n  --name copy-and-run-$(date +%H%M%S) --location \"$LOC\" --timeout-in-seconds 600 \\\n  --script \"$(cat vm-run.sh)\" \\\n  --protected-parameters \"PSENV=$(base64 \u003C .env | tr -d '\\n')\" \\\n                         \"PSHDB=$(base64 \u003C .env.horizondb | tr -d '\\n')\" \\\n                         \"PSMP=$(base64 \u003C .model_providers.json | tr -d '\\n')\"\naz vm run-command delete --subscription \"$SUB\" -g \"$RG\" --vm-name \"$VM\" --name \u003Cthat-name> --yes\n```\n\n- Do **not** pass `--run-as-user`: with it, the parameters do not reach the\n  script. Run as root, and inside the script write the files, then\n  `chown azureuser:azureuser` and `chmod 600` them.\n- The parameters arrive as environment variables: `printf '%s' \"$PSENV\" | base64 -d > .env`.\n- Start the run as the user: `sudo -u azureuser -H setsid nohup \u003Cscript> &`.\n- Delete the run command afterwards.\n\n## Gotchas\n\n- **`pkill -f run-tests.sh` over SSH kills your own SSH command,** whose text\n  contains the pattern. Use `pkill -f \"[r]un-tests.sh\"`.\n- **zsh reads `$name:a` as a path modifier.** `\"image-$svc:tag\"` becomes a\n  file path. Write `\"image-${svc}:tag\"`.\n- **Two HorizonDB tests sit close to their 60 s limit:** the first test of\n  `delete-cascade` and of `pg-migrator` sets up a fresh schema (about 55 s).\n  Under the full parallel load they can time out. Re-run them alone with\n  `--with-horizondb --suite=delete-cascade --suite=pg-migrator` before calling\n  it a failure.\n- **Which keys the run uses:**\n  - Model calls go through the test catalog\n    (`packages\u002Fsdk\u002Ftest\u002Ffixtures\u002Fmodel-providers.test.json`), that is GitHub\n    Copilot with `GITHUB_TOKEN`.\n  - The HorizonDB phase also calls the embedding endpoint set by the\n    `HORIZON_EMBED_*` values in `.env.horizondb`.\n- **A test that prints a database URL must hide the password.**\n  `packages\u002Fsdk\u002Ftest\u002Fhelpers\u002Flocal-env.js` does; keep it that way, because\n  the run log stays on the VM.\n\n## Tear down\n\n```bash\naz group delete --subscription \"$SUB\" -n \"$RG\" --yes --no-wait\n```\n\nThis deletes the VM, its disk, IP and network, and any credentials still on\nit. Remove the SSH alias and key if you will not create the VM again.\n","# Azure test runner VM\n\nA plain Ubuntu VM in the same Azure region as the HorizonDB server. It runs\nthe full all-providers suite from a clone of the repo. It is not a GitHub\nActions runner; for that, see the `Manage Azure CI runner` workflow and\n`deploy\u002Fproviders\u002Fazure\u002Fci\u002Frunner\u002FREADME.md`.\n\n## Why\n\nThe HorizonDB phase makes many short database calls in a row. Distance\ndecides whether it passes:\n\n| Where the tests run | One `SELECT 1` | Result |\n|---|---|---|\n| A laptop on another continent | 130–160 ms | Most HorizonDB SDK tests hit their time limits |\n| A VM in the server's region | 0–1 ms | The full suite passes |\n\nMeasure it first (step 6) when in doubt.\n\n## What you need\n\n- Azure CLI signed in, with rights to create a resource group and a VM.\n- The repo's local test config files: `.env`, `.env.horizondb` and\n  `.model_providers.json`. They hold credentials (a GitHub token, model keys,\n  the HorizonDB connection). See \"Credentials\" below before copying them.\n\nSet these once in your shell. Use your own values:\n\n```bash\nSUB=\u003Csubscription-id>\nRG=\u003Cresource-group>            # a new, dedicated group, e.g. pilotswarm-testrunner-rg\nVM=\u003Cvm-name>                   # e.g. ps-testrunner\nLOC=\u003Chorizondb-region>         # the region of your HorizonDB server\nKEY=~\u002F.ssh\u002Fid_ed25519_ps_testrunner\nMYIP=$(curl -s https:\u002F\u002Fapi.ipify.org)\n```\n\n## 1. SSH key and setup script\n\n```bash\n[ -f \"$KEY\" ] || ssh-keygen -q -t ed25519 -N '' -C 'pilotswarm test runner' -f \"$KEY\"\n\ncat > cloud-init.yaml \u003C\u003C'EOF'\n#cloud-config\npackage_update: true\npackages: [git, build-essential, python3, jq, ca-certificates, curl, docker.io, unzip]\nruncmd:\n  - curl -fsSL https:\u002F\u002Fdeb.nodesource.com\u002Fsetup_24.x | bash -\n  - apt-get install -y nodejs\n  - usermod -aG docker azureuser\n  - systemctl enable --now docker\nEOF\n```\n\n## 2. Create the VM\n\nUse 8 cores. The suite runs 8 test files at once, each with its own workers,\nplus PostgreSQL in Docker. On 4 cores the tests compete for CPU and hit their\ntime limits.\n\n```bash\naz group create --subscription \"$SUB\" -n \"$RG\" -l \"$LOC\" -o none\naz vm create --subscription \"$SUB\" -g \"$RG\" -n \"$VM\" \\\n  --image Canonical:ubuntu-24_04-lts:server:latest \\\n  --size Standard_D8s_v5 --os-disk-size-gb 128 --storage-sku Premium_LRS \\\n  --admin-username azureuser --ssh-key-values \"$KEY.pub\" --authentication-type ssh \\\n  --public-ip-sku Standard --nsg-rule NONE --custom-data cloud-init.yaml \\\n  --query publicIpAddress -o tsv\n```\n\nWrite down the IP it prints.\n\n## 3. SSH from your IP only, and a nightly shutdown\n\n```bash\nNSG=$(az network nsg list --subscription \"$SUB\" -g \"$RG\" --query '[0].name' -o tsv)\naz network nsg rule create --subscription \"$SUB\" -g \"$RG\" --nsg-name \"$NSG\" \\\n  -n ssh-from-me --priority 1000 --direction Inbound --access Allow --protocol Tcp \\\n  --source-address-prefixes \"$MYIP\u002F32\" --destination-port-ranges 22 -o none\naz vm auto-shutdown --subscription \"$SUB\" -g \"$RG\" -n \"$VM\" --time 0200 -o none   # UTC\n```\n\nAdd an alias to `~\u002F.ssh\u002Fconfig`:\n\n```\nHost ps-testrunner\n    HostName \u003Cvm-ip>\n    User azureuser\n    IdentityFile ~\u002F.ssh\u002Fid_ed25519_ps_testrunner\n    IdentitiesOnly yes\n    StrictHostKeyChecking accept-new\n    ServerAliveInterval 30\n```\n\nThen wait for the setup script: `ssh ps-testrunner 'cloud-init status --wait; node --version; docker --version'`.\n\n**If SSH times out** while the VM runs, check the security rules:\n`az network nsg rule list -g \"$RG\" --nsg-name \"$NSG\" -o table`. Some\norganizations add their own deny rule for port 22 from the internet, with a\nhigher priority than yours. Do not edit those rules. Connect from the network\nthey allow, or drive the VM with Run Command (see \"Without SSH\").\n\n## 4. The repo\n\n```bash\nssh ps-testrunner 'git clone -q https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm.git pilotswarm\n  cd pilotswarm && git checkout -q -B \u003Cbranch> origin\u002F\u003Cbranch> && npm ci --no-audit --no-fund'\n```\n\nTo test another commit later: `git fetch origin \u003Cbranch> && git checkout -B \u003Cbranch> origin\u002F\u003Cbranch> && npm ci`.\n\n## 5. PostgreSQL\n\nThe container's password must match `DATABASE_URL` in the `.env` you copy.\n`run-tests.sh` refuses to run with fewer than 1500 connections.\n\n```bash\nssh ps-testrunner 'docker run -d --name pilotswarm-pg --restart unless-stopped \\\n    -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=\u003Cpassword-from-DATABASE_URL> \\\n    -e POSTGRES_DB=pilotswarm -p 127.0.0.1:5432:5432 postgres:16 \\\n  && sleep 5 \\\n  && docker exec pilotswarm-pg psql -U postgres -qc \"ALTER SYSTEM SET max_connections = 1500;\" \\\n  && docker restart pilotswarm-pg'\n```\n\n## 6. Credentials: copy for one run, delete after\n\nThe three config files hold real credentials. Keep them on the VM only while a\nrun needs them:\n\n1. Copy them right before the run.\n2. In the same step, start a watcher that deletes them when the run ends.\n3. Never leave them between runs. Ask the person before copying them to a\n   machine that did not hold them before.\n\n**With SSH:**\n\n```bash\nscp .env .env.horizondb .model_providers.json ps-testrunner:pilotswarm\u002F\nssh ps-testrunner 'cd pilotswarm && chmod 600 .env .env.horizondb .model_providers.json\n  grep -q \"^HORIZON_DATABASE_URL=.\" .env.horizondb && echo \"hdb config: ok\"'\n```\n\nCheck the HorizonDB latency once:\n\n```bash\nssh ps-testrunner 'cd pilotswarm && node --env-file=.env.horizondb -e \"\nconst pg = require(\\\"pg\\\");\n(async () => {\n  const u = new URL(process.env.HORIZON_DATABASE_URL);\n  u.searchParams.delete(\\\"sslmode\\\"); u.searchParams.delete(\\\"uselibpqcompat\\\");\n  const c = new pg.Client({ connectionString: u.toString(), ssl: { rejectUnauthorized: false } });\n  await c.connect();\n  const t = []; for (let i = 0; i \u003C 5; i++) { const s = Date.now(); await c.query(\\\"SELECT 1\\\"); t.push(Date.now() - s); }\n  console.log(\\\"SELECT 1 ms:\\\", t.join(\\\", \\\")); await c.end();\n})().catch((e) => console.log(\\\"error:\\\", e.code || e.message));\"'\n```\n\n## 7. Run the suite\n\nStart it detached, so it keeps going when your laptop sleeps. Then start the\nwatcher that deletes the credentials when the run ends.\n`PS_TEST_SKIP_STALE_CLEANUP=1` skips the global sweep of old test schemas, so\nthe run cannot drop schemas another run on the same HorizonDB still uses.\n\n```bash\nssh ps-testrunner 'cd pilotswarm\n  PS_TEST_SKIP_STALE_CLEANUP=1 setsid nohup bash -c \".\u002Fscripts\u002Frun-tests.sh --all-providers > ~\u002Fall-providers.log 2>&1; echo EXIT=\\$? >> ~\u002Fall-providers.log\" > \u002Fdev\u002Fnull 2>&1 \u003C \u002Fdev\u002Fnull &\n  sleep 3\n  setsid nohup bash -c \"while pgrep -f \\\"[r]un-tests.sh --all-providers\\\" > \u002Fdev\u002Fnull; do sleep 30; done; shred -u ~\u002Fpilotswarm\u002F.env ~\u002Fpilotswarm\u002F.env.horizondb ~\u002Fpilotswarm\u002F.model_providers.json; echo \\\"credentials removed \\$(date -u)\\\" >> ~\u002Fsecrets-cleanup.log\" > \u002Fdev\u002Fnull 2>&1 \u003C \u002Fdev\u002Fnull &\n  echo started'\n```\n\nProgress and result:\n\n```bash\nssh ps-testrunner 'sed \"s\u002F\\x1b\\[[0-9;]*m\u002F\u002Fg\" ~\u002Fall-providers.log | grep -aE \"Provider phase|Test Files|Tests  |Combined provider result|EXIT=\"'\n```\n\nA full run takes about 35–40 minutes: the stock PostgreSQL pass (about 22),\nthen the HorizonDB phase. To run only some files on HorizonDB:\n`.\u002Fscripts\u002Frun-tests.sh --with-horizondb --suite=\u003Cname> --suite=\u003Cname>`.\n\n## Without SSH: Run Command\n\nUse `az vm run-command invoke` for commands that carry no secret (status,\nlogs, pkill). It returns the output, but Azure keeps the script, so **never\nput credentials in it**.\n\nTo copy the credential files, use Run Command v2 with protected parameters.\nAzure stores them encrypted and never returns them:\n\n```bash\naz vm run-command create --subscription \"$SUB\" -g \"$RG\" --vm-name \"$VM\" \\\n  --name copy-and-run-$(date +%H%M%S) --location \"$LOC\" --timeout-in-seconds 600 \\\n  --script \"$(cat vm-run.sh)\" \\\n  --protected-parameters \"PSENV=$(base64 \u003C .env | tr -d '\\n')\" \\\n                         \"PSHDB=$(base64 \u003C .env.horizondb | tr -d '\\n')\" \\\n                         \"PSMP=$(base64 \u003C .model_providers.json | tr -d '\\n')\"\naz vm run-command delete --subscription \"$SUB\" -g \"$RG\" --vm-name \"$VM\" --name \u003Cthat-name> --yes\n```\n\n- Do **not** pass `--run-as-user`: with it, the parameters do not reach the\n  script. Run as root, and inside the script write the files, then\n  `chown azureuser:azureuser` and `chmod 600` them.\n- The parameters arrive as environment variables: `printf '%s' \"$PSENV\" | base64 -d > .env`.\n- Start the run as the user: `sudo -u azureuser -H setsid nohup \u003Cscript> &`.\n- Delete the run command afterwards.\n\n## Gotchas\n\n- **`pkill -f run-tests.sh` over SSH kills your own SSH command,** whose text\n  contains the pattern. Use `pkill -f \"[r]un-tests.sh\"`.\n- **zsh reads `$name:a` as a path modifier.** `\"image-$svc:tag\"` becomes a\n  file path. Write `\"image-${svc}:tag\"`.\n- **Two HorizonDB tests sit close to their 60 s limit:** the first test of\n  `delete-cascade` and of `pg-migrator` sets up a fresh schema (about 55 s).\n  Under the full parallel load they can time out. Re-run them alone with\n  `--with-horizondb --suite=delete-cascade --suite=pg-migrator` before calling\n  it a failure.\n- **Which keys the run uses:**\n  - Model calls go through the test catalog\n    (`packages\u002Fsdk\u002Ftest\u002Ffixtures\u002Fmodel-providers.test.json`), that is GitHub\n    Copilot with `GITHUB_TOKEN`.\n  - The HorizonDB phase also calls the embedding endpoint set by the\n    `HORIZON_EMBED_*` values in `.env.horizondb`.\n- **A test that prints a database URL must hide the password.**\n  `packages\u002Fsdk\u002Ftest\u002Fhelpers\u002Flocal-env.js` does; keep it that way, because\n  the run log stays on the VM.\n\n## Tear down\n\n```bash\naz group delete --subscription \"$SUB\" -n \"$RG\" --yes --no-wait\n```\n\nThis deletes the VM, its disk, IP and network, and any credentials still on\nit. Remove the SSH alias and key if you will not create the VM again.\n",{"name":5,"description":7},"main","cecafe1ca7a9f2778a6166abdd12f6165d99f777",".github\u002Fskills\u002Fazure-ps-test-runner-vm",10,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-13 02:04:17",{"source":8,"skillName":5,"commands":60,"prompt":70,"zipAvailable":71},[61,64,67],{"tool":62,"command":63},"npx","npx skills add microsoft\u002FPilotSwarm --skill azure-ps-test-runner-vm",{"tool":65,"command":66},"bunx","bunx skills add microsoft\u002FPilotSwarm --skill azure-ps-test-runner-vm",{"tool":68,"command":69},"pnpm","pnpm dlx skills add microsoft\u002FPilotSwarm --skill azure-ps-test-runner-vm","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fazure-ps-test-runner-vm\u002FSKILL.md",true,[],[74,95,116,131,156,178],{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":45},"01M69A9CY8CXHED3AFC8DAT9G0","pilotswarm-three-tier","Reference for adding a Tier 3 worker AKS cluster to a PilotSwarm deployment. Covers provisioning, identity, Dockerfile changes, entrypoint, manifests, and verification.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-three-tier\u002FSKILL.md",[],[81,82,83,84,35,85,86,87,88,89,90,91,92,93,94],"pilotswarm three tier","three","entrypoint","manifests","changes","aks","verification","deployment","reference","dockerfile","identity","provisioning","worker","cluster",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":45},"01M69A9B6R5H88HJFEYC0P4R57","pilotswarm-sdk-builder","Use when creating or updating an SDK-first app on top of PilotSwarm. Covers the client\u002Fworker split, plugin layering, tool registration, tests, and the DevOps sample structure.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-sdk-builder\u002FSKILL.md",[],[102,103,104,105,106,107,41,108,31,109,110,111,37,93,112,113,114,115],"sdk","pilotswarm sdk builder","builder","tool","first","plugin","client","top","split","app","sample","structure","registration","devops",{"skillNo":117,"name":118,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":122,"publishedAt":45},"01M69A99A43M97VTYZBCN1JQ1H","pilotswarm-portal-builder","Use when creating or updating a browser portal on top of PilotSwarm. Covers portal branding, plugin.json.portal, named-agent exposure, auth add-ons, and remote deployment wiring.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-portal-builder\u002FSKILL.md",[],[123,104,124,31,125,109,126,127,41,128,129,88,107,130],"pilotswarm portal builder","remote","wiring","branding","add","json","browser","auth",{"skillNo":132,"name":133,"nameZh":6,"description":134,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":135,"categories":136,"keywords":140,"publishedAt":45},"01M69A97JZY10V1VE12NF4JR5A","pilotswarm-knowledge-harvester","Use when an SDK or CLI app needs to ingest external sources into durable, searchable knowledge and an open knowledge graph. Covers enabling the EnhancedFactStore + GraphStore providers, authoring a crawler:true agent, the crawl→graph→reader flow, ACL\u002FscopeKey evidence, and tests.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-knowledge-harvester\u002FSKILL.md",[137],{"categoryNo":138,"categoryName":139,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[141,142,41,31,143,144,145,146,111,147,37,148,149,150,151,152,153,154,102,155],"knowledge","pilotswarm knowledge harvester","cli app","true","crawler","sources","authoring","crawl","ingest","evidence","reader","open","flow","knowledge graph","cli",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":165,"publishedAt":45},"01M69A95Y3EWHSZ1TDFMXWAKX3","pilotswarm-hybrid-datastore","Use when a PilotSwarm app needs stock PostgreSQL for runtime storage plus HorizonDB for enhanced facts, semantic\u002Flexical search, and the knowledge graph. Covers env topology, horizonConfigFromEnv wiring, harvester agents, Azure secret\u002Fresource guidance, and the rule that Docker defaults remain stock Postgres.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-hybrid-datastore\u002FSKILL.md",[162],{"categoryNo":163,"categoryName":164,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署",[166,167,168,111,169,170,171,172,41,141,154,173,174,27,175,176,31,125,35,177],"hybrid","pilotswarm hybrid datastore","storage","runtime","stock","postgres","guidance","search","postgresql","env","docker","facts",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":184,"publishedAt":45},"01M69A94F4WR5CXDZ73N658DV0","pilotswarm-duroxide-versioning","Use when changing PilotSwarm durable orchestration behavior, continue-as-new state, or replay-sensitive control flow.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FPilotSwarm\u002Fblob\u002Fmain\u002Ftemplates\u002Fbuilder-agents\u002Fskills\u002Fpilotswarm-duroxide-versioning\u002FSKILL.md",[],[185,186,187,188,31,189,190,191,41,192,193,153,194],"pilotswarm duroxide versioning","versioning","orchestration","new","as","control","replay","behavior","continue","state"]