@indigoai-us/hq-cli 5.60.0 → 5.62.0
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.
- package/dist/commands/agents.d.ts +109 -0
- package/dist/commands/agents.js +385 -0
- package/dist/commands/db-migrate.d.ts +6 -0
- package/dist/commands/db-migrate.js +42 -0
- package/dist/commands/db-provision.d.ts +15 -0
- package/dist/commands/db-provision.js +78 -0
- package/dist/commands/db-sql.d.ts +9 -0
- package/dist/commands/db-sql.js +81 -0
- package/dist/commands/db-status.d.ts +7 -0
- package/dist/commands/db-status.js +70 -0
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +23 -0
- package/dist/commands/integrations.d.ts +78 -0
- package/dist/commands/integrations.js +309 -0
- package/dist/commands/members.js +4 -4
- package/dist/commands/outposts.d.ts +60 -0
- package/dist/commands/outposts.js +255 -0
- package/dist/commands/pack-install.d.ts +7 -1
- package/dist/commands/pack-install.js +86 -15
- package/dist/commands/packs.d.ts +2 -1
- package/dist/commands/packs.js +13 -8
- package/dist/commands/secrets.d.ts +13 -0
- package/dist/commands/secrets.js +149 -10
- package/dist/commands/skill.d.ts +153 -0
- package/dist/commands/skill.js +593 -0
- package/dist/commands/workers.d.ts +48 -0
- package/dist/commands/workers.js +229 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +14 -240
- package/dist/lib/db/control-plane.d.ts +45 -0
- package/dist/lib/db/control-plane.js +81 -0
- package/dist/lib/db/local.d.ts +49 -0
- package/dist/lib/db/local.js +106 -0
- package/dist/lib/db/migrate.d.ts +41 -0
- package/dist/lib/db/migrate.js +104 -0
- package/dist/lib/db/paths.d.ts +56 -0
- package/dist/lib/db/paths.js +103 -0
- package/dist/lib/db/remote-engine.d.ts +58 -0
- package/dist/lib/db/remote-engine.js +90 -0
- package/dist/lib/db/remote-sql.d.ts +22 -0
- package/dist/lib/db/remote-sql.js +39 -0
- package/dist/lib/db/sql.d.ts +49 -0
- package/dist/lib/db/sql.js +132 -0
- package/dist/main.d.ts +7 -0
- package/dist/main.js +272 -0
- package/dist/utils/cognito-session.js +3 -3
- package/dist/utils/sandbox-runner-client.d.ts +13 -0
- package/dist/utils/sandbox-runner-client.js +83 -6
- package/dist/utils/version-check.d.ts +6 -0
- package/dist/utils/version-check.js +78 -2
- package/package.json +9 -1
- package/pnpm-workspace.yaml +2 -0
- package/src/commands/agents.test.ts +297 -0
- package/src/commands/agents.ts +561 -0
- package/src/commands/db-migrate.ts +55 -0
- package/src/commands/db-provision.ts +102 -0
- package/src/commands/db-sql.ts +124 -0
- package/src/commands/db-status.ts +100 -0
- package/src/commands/db.ts +26 -0
- package/src/commands/integrations.test.ts +284 -0
- package/src/commands/integrations.ts +438 -0
- package/src/commands/members.ts +2 -2
- package/src/commands/outposts.test.ts +177 -0
- package/src/commands/outposts.ts +338 -0
- package/src/commands/pack-install.ts +115 -18
- package/src/commands/pack-update-cache.test.ts +149 -0
- package/src/commands/packs.ts +28 -7
- package/src/commands/secrets.parse-destination.test.ts +38 -0
- package/src/commands/secrets.test.ts +342 -0
- package/src/commands/secrets.ts +227 -13
- package/src/commands/skill.test.ts +770 -0
- package/src/commands/skill.ts +796 -0
- package/src/commands/workers.test.ts +158 -0
- package/src/commands/workers.ts +298 -0
- package/src/index.test.ts +32 -0
- package/src/index.ts +11 -274
- package/src/lib/db/control-plane.test.ts +59 -0
- package/src/lib/db/control-plane.ts +113 -0
- package/src/lib/db/local.test.ts +81 -0
- package/src/lib/db/local.ts +148 -0
- package/src/lib/db/migrate.test.ts +133 -0
- package/src/lib/db/migrate.ts +137 -0
- package/src/lib/db/paths.test.ts +112 -0
- package/src/lib/db/paths.ts +128 -0
- package/src/lib/db/remote-engine.test.ts +44 -0
- package/src/lib/db/remote-engine.ts +148 -0
- package/src/lib/db/remote-sql.test.ts +32 -0
- package/src/lib/db/remote-sql.ts +62 -0
- package/src/lib/db/sql.test.ts +106 -0
- package/src/lib/db/sql.ts +192 -0
- package/src/main.ts +314 -0
- package/src/utils/cognito-session.ts +1 -1
- package/src/utils/sandbox-runner-client.test.ts +128 -0
- package/src/utils/sandbox-runner-client.ts +100 -4
- package/src/utils/version-check.test.ts +30 -0
- package/src/utils/version-check.ts +72 -0
- package/test/commands/db-tenant-isolation.test.ts +94 -0
- package/test/commands/db.test.ts +85 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f7058fc6-251e-5e11-af6c-de15f6e256f3")}catch(e){}}();
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import * as path from "path";
|
|
@@ -9,10 +9,15 @@ import { CLI_VERSION } from "../cli-version.js";
|
|
|
9
9
|
const PACKAGE_NAME = "@indigoai-us/hq-cli";
|
|
10
10
|
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
11
11
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const CACHE_TTL_JITTER_MS = 60 * 60 * 1000;
|
|
12
13
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
14
|
+
const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
13
15
|
function cachePath() {
|
|
14
16
|
return path.join(os.homedir(), ".hq", "version-check.json");
|
|
15
17
|
}
|
|
18
|
+
function lockPath() {
|
|
19
|
+
return path.join(os.homedir(), ".hq", "version-check.lock");
|
|
20
|
+
}
|
|
16
21
|
function isOptedOut() {
|
|
17
22
|
return process.env.HQ_NO_UPDATE_CHECK === "1";
|
|
18
23
|
}
|
|
@@ -40,6 +45,59 @@ function writeCache(entry) {
|
|
|
40
45
|
// best-effort; never break the CLI on cache write failure
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
function freshEnough(entry, now = Date.now()) {
|
|
49
|
+
const jitter = Math.floor(Math.random() * CACHE_TTL_JITTER_MS);
|
|
50
|
+
return now - entry.fetchedAt <= CACHE_TTL_MS - jitter;
|
|
51
|
+
}
|
|
52
|
+
function isKnownNoninteractiveStatusProbe(argv = process.argv) {
|
|
53
|
+
const args = argv.slice(2);
|
|
54
|
+
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
55
|
+
const json = args.includes("--json") || !process.stdout.isTTY;
|
|
56
|
+
if (!json)
|
|
57
|
+
return false;
|
|
58
|
+
if (positional[0] === "mcp" && positional[1] === "status")
|
|
59
|
+
return true;
|
|
60
|
+
if (positional[0] === "packs" && (positional[1] === "list" || positional[1] === "ls"))
|
|
61
|
+
return true;
|
|
62
|
+
if (positional[0] === "packages" &&
|
|
63
|
+
positional[1] === "packs" &&
|
|
64
|
+
(positional[2] === "list" || positional[2] === "ls")) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if ((positional[0] === "sources" || positional[0] === "signals") && positional[1] === "list") {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function acquireRefreshLock(now = Date.now()) {
|
|
73
|
+
const dir = lockPath();
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
76
|
+
fs.mkdirSync(dir);
|
|
77
|
+
fs.writeFileSync(path.join(dir, "owner"), `${process.pid}\n${now}\n`);
|
|
78
|
+
return () => {
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// best-effort lock cleanup
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
try {
|
|
89
|
+
const stat = fs.statSync(dir);
|
|
90
|
+
if (now - stat.mtimeMs > REFRESH_LOCK_STALE_MS) {
|
|
91
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
return acquireRefreshLock(now);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore lock inspection failures
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
43
101
|
export function maybeWarnNewVersion() {
|
|
44
102
|
if (isOptedOut())
|
|
45
103
|
return;
|
|
@@ -60,7 +118,18 @@ export function maybeWarnNewVersion() {
|
|
|
60
118
|
export async function refreshVersionCache() {
|
|
61
119
|
if (isOptedOut())
|
|
62
120
|
return;
|
|
121
|
+
if (isKnownNoninteractiveStatusProbe())
|
|
122
|
+
return;
|
|
123
|
+
const existing = readCache();
|
|
124
|
+
if (existing && freshEnough(existing))
|
|
125
|
+
return;
|
|
126
|
+
const releaseLock = acquireRefreshLock();
|
|
127
|
+
if (!releaseLock)
|
|
128
|
+
return;
|
|
63
129
|
try {
|
|
130
|
+
const lockedExisting = readCache();
|
|
131
|
+
if (lockedExisting && freshEnough(lockedExisting))
|
|
132
|
+
return;
|
|
64
133
|
const res = await fetch(REGISTRY_URL, {
|
|
65
134
|
headers: { Accept: "application/json" },
|
|
66
135
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
@@ -75,6 +144,13 @@ export async function refreshVersionCache() {
|
|
|
75
144
|
catch {
|
|
76
145
|
// best-effort; offline / registry down / timeout — silent
|
|
77
146
|
}
|
|
147
|
+
finally {
|
|
148
|
+
releaseLock();
|
|
149
|
+
}
|
|
78
150
|
}
|
|
151
|
+
export const __test__ = {
|
|
152
|
+
CACHE_TTL_MS,
|
|
153
|
+
isKnownNoninteractiveStatusProbe,
|
|
154
|
+
};
|
|
79
155
|
//# sourceMappingURL=version-check.js.map
|
|
80
|
-
//# debugId=
|
|
156
|
+
//# debugId=f7058fc6-251e-5e11-af6c-de15f6e256f3
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.62.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
13
|
"lint": "eslint .",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
+
"test:db": "vitest run src/lib/db test/commands/db.test.ts test/commands/db-tenant-isolation.test.ts",
|
|
15
16
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
16
17
|
"coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
17
18
|
"vitest": "vitest",
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"@indigoai-us/hq-cloud": "^6.12.1",
|
|
22
23
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
23
24
|
"@sentry/node": "^10.49.0",
|
|
25
|
+
"better-sqlite3": "^12.11.1",
|
|
24
26
|
"chalk": "^5.3.0",
|
|
25
27
|
"commander": "^12.1.0",
|
|
26
28
|
"js-yaml": "^4.1.0",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
35
37
|
"@eslint/js": "^10.0.1",
|
|
38
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
36
39
|
"@types/js-yaml": "^4.0.9",
|
|
37
40
|
"@types/node": "^22.0.0",
|
|
38
41
|
"@types/semver": "^7.5.8",
|
|
@@ -58,5 +61,10 @@
|
|
|
58
61
|
"type": "module",
|
|
59
62
|
"engines": {
|
|
60
63
|
"node": ">=20.0.0"
|
|
64
|
+
},
|
|
65
|
+
"pnpm": {
|
|
66
|
+
"onlyBuiltDependencies": [
|
|
67
|
+
"better-sqlite3"
|
|
68
|
+
]
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `hq agents` (agents.ts).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors company.test.ts / members.test.ts: mock ensureCognitoToken +
|
|
5
|
+
* getCompanyUid, spy on global fetch, drive through a Commander program, and
|
|
6
|
+
* assert the /v1/agents request shape (method + path + body) plus the guard /
|
|
7
|
+
* validation / exit behavior. `vaultApiFetch` itself is NOT mocked — the spied
|
|
8
|
+
* global fetch verifies the real URL + init the helper produces.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import {
|
|
13
|
+
afterEach,
|
|
14
|
+
beforeEach,
|
|
15
|
+
describe,
|
|
16
|
+
expect,
|
|
17
|
+
it,
|
|
18
|
+
vi,
|
|
19
|
+
type MockInstance,
|
|
20
|
+
} from "vitest";
|
|
21
|
+
|
|
22
|
+
vi.mock("../utils/cognito-session.js", async (importOriginal) => {
|
|
23
|
+
const original =
|
|
24
|
+
await importOriginal<typeof import("../utils/cognito-session.js")>();
|
|
25
|
+
return {
|
|
26
|
+
...original,
|
|
27
|
+
ensureCognitoToken: vi.fn(async () => "test-token"),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
vi.mock("../utils/vault-api.js", async (importOriginal) => {
|
|
32
|
+
const original = await importOriginal<typeof import("../utils/vault-api.js")>();
|
|
33
|
+
return {
|
|
34
|
+
...original,
|
|
35
|
+
getCompanyUid: vi.fn(async () => "cmp_acme"),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
40
|
+
import { getCompanyUid } from "../utils/vault-api.js";
|
|
41
|
+
import { registerAgentsCommand } from "./agents.js";
|
|
42
|
+
|
|
43
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
44
|
+
return new Response(JSON.stringify(body), {
|
|
45
|
+
status,
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let fetchSpy: MockInstance<typeof fetch>;
|
|
51
|
+
let exitSpy: MockInstance<typeof process.exit>;
|
|
52
|
+
let logSpy: MockInstance<typeof console.log>;
|
|
53
|
+
const mockEnsureCognitoToken = vi.mocked(ensureCognitoToken);
|
|
54
|
+
const mockGetCompanyUid = vi.mocked(getCompanyUid);
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
59
|
+
mockEnsureCognitoToken.mockResolvedValue("test-token");
|
|
60
|
+
mockGetCompanyUid.mockResolvedValue("cmp_acme");
|
|
61
|
+
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: number) => {
|
|
62
|
+
throw new Error(`process.exit(${code})`);
|
|
63
|
+
}) as unknown as MockInstance<typeof process.exit>;
|
|
64
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
65
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.restoreAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function buildProgram(): Command {
|
|
73
|
+
const program = new Command();
|
|
74
|
+
program.name("hq").exitOverride();
|
|
75
|
+
registerAgentsCommand(program);
|
|
76
|
+
return program;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function run(args: string[]): Promise<void> {
|
|
80
|
+
await buildProgram().parseAsync(["node", "hq", ...args]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("hq agents list", () => {
|
|
84
|
+
it("GETs /v1/agents with the resolved companyUid", async () => {
|
|
85
|
+
fetchSpy.mockResolvedValueOnce(
|
|
86
|
+
jsonResponse(200, {
|
|
87
|
+
agents: [
|
|
88
|
+
{ uid: "agt_1", name: "Ops", slug: "ops", agentStatus: "running" },
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await run(["agents", "--company", "acme", "list"]);
|
|
94
|
+
|
|
95
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
96
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
97
|
+
expect(String(url)).toContain("/v1/agents");
|
|
98
|
+
expect(String(url)).toContain("companyUid=cmp_acme");
|
|
99
|
+
expect(init?.method ?? "GET").toBe("GET");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("accepts --company on the subcommand too", async () => {
|
|
103
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { agents: [] }));
|
|
104
|
+
await run(["agents", "list", "--company", "acme"]);
|
|
105
|
+
expect(mockGetCompanyUid).toHaveBeenCalledWith("test-token", "acme");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("emits raw JSON with --json", async () => {
|
|
109
|
+
const stdoutSpy = vi
|
|
110
|
+
.spyOn(process.stdout, "write")
|
|
111
|
+
.mockImplementation(() => true);
|
|
112
|
+
fetchSpy.mockResolvedValueOnce(
|
|
113
|
+
jsonResponse(200, { agents: [{ uid: "agt_1", name: "Ops", slug: "ops" }] }),
|
|
114
|
+
);
|
|
115
|
+
await run(["agents", "--company", "acme", "list", "--json"]);
|
|
116
|
+
const printed = stdoutSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
117
|
+
expect(printed).toContain('"uid": "agt_1"');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("prints a friendly message when agents are not enabled (404)", async () => {
|
|
121
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(404, { error: "not found" }));
|
|
122
|
+
await run(["agents", "--company", "acme", "list"]);
|
|
123
|
+
const printed = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
124
|
+
expect(printed).toMatch(/agents feature isn't enabled/i);
|
|
125
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("hq agents rename", () => {
|
|
130
|
+
it("PATCHes the profile with displayName", async () => {
|
|
131
|
+
fetchSpy.mockResolvedValueOnce(
|
|
132
|
+
jsonResponse(200, {
|
|
133
|
+
uid: "agt_1",
|
|
134
|
+
profile: { displayName: "New" },
|
|
135
|
+
slackUpdated: false,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await run(["agents", "--company", "acme", "rename", "agt_1", "New"]);
|
|
140
|
+
|
|
141
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
142
|
+
expect(String(url)).toContain("/v1/agents/agt_1/profile");
|
|
143
|
+
expect(init?.method).toBe("PATCH");
|
|
144
|
+
expect(JSON.parse(init?.body as string)).toEqual({ displayName: "New" });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("hq agents set", () => {
|
|
149
|
+
it("PATCHes only the supplied profile fields", async () => {
|
|
150
|
+
fetchSpy.mockResolvedValueOnce(
|
|
151
|
+
jsonResponse(200, { uid: "agt_1", profile: {}, slackUpdated: true }),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await run([
|
|
155
|
+
"agents",
|
|
156
|
+
"--company",
|
|
157
|
+
"acme",
|
|
158
|
+
"set",
|
|
159
|
+
"agt_1",
|
|
160
|
+
"--title",
|
|
161
|
+
"Chief of Staff",
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
165
|
+
expect(String(url)).toContain("/v1/agents/agt_1/profile");
|
|
166
|
+
expect(init?.method).toBe("PATCH");
|
|
167
|
+
const sent = JSON.parse(init?.body as string);
|
|
168
|
+
expect(sent).toEqual({ title: "Chief of Staff" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("requires at least one field", async () => {
|
|
172
|
+
await expect(
|
|
173
|
+
run(["agents", "--company", "acme", "set", "agt_1"]),
|
|
174
|
+
).rejects.toThrow("process.exit(1)");
|
|
175
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("hq agents config", () => {
|
|
180
|
+
it("PATCHes runtime-config with model/effort/tier", async () => {
|
|
181
|
+
fetchSpy.mockResolvedValueOnce(
|
|
182
|
+
jsonResponse(200, { uid: "agt_1", codexModel: "gpt-5.5", applied: true }),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await run([
|
|
186
|
+
"agents",
|
|
187
|
+
"--company",
|
|
188
|
+
"acme",
|
|
189
|
+
"config",
|
|
190
|
+
"agt_1",
|
|
191
|
+
"--model",
|
|
192
|
+
"gpt-5.5",
|
|
193
|
+
"--effort",
|
|
194
|
+
"high",
|
|
195
|
+
"--tier",
|
|
196
|
+
"priority",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
200
|
+
expect(String(url)).toContain("/v1/agents/agt_1/runtime-config");
|
|
201
|
+
expect(init?.method).toBe("PATCH");
|
|
202
|
+
expect(JSON.parse(init?.body as string)).toEqual({
|
|
203
|
+
codexModel: "gpt-5.5",
|
|
204
|
+
codexReasoningEffort: "high",
|
|
205
|
+
codexServiceTier: "priority",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("rejects an invalid --effort", async () => {
|
|
210
|
+
await expect(
|
|
211
|
+
run([
|
|
212
|
+
"agents",
|
|
213
|
+
"--company",
|
|
214
|
+
"acme",
|
|
215
|
+
"config",
|
|
216
|
+
"agt_1",
|
|
217
|
+
"--effort",
|
|
218
|
+
"turbo",
|
|
219
|
+
]),
|
|
220
|
+
).rejects.toThrow("process.exit(1)");
|
|
221
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("rejects an invalid --tier", async () => {
|
|
225
|
+
await expect(
|
|
226
|
+
run([
|
|
227
|
+
"agents",
|
|
228
|
+
"--company",
|
|
229
|
+
"acme",
|
|
230
|
+
"config",
|
|
231
|
+
"agt_1",
|
|
232
|
+
"--tier",
|
|
233
|
+
"turbo",
|
|
234
|
+
]),
|
|
235
|
+
).rejects.toThrow("process.exit(1)");
|
|
236
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("requires at least one field", async () => {
|
|
240
|
+
await expect(
|
|
241
|
+
run(["agents", "--company", "acme", "config", "agt_1"]),
|
|
242
|
+
).rejects.toThrow("process.exit(1)");
|
|
243
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("hq agents start/stop/retry", () => {
|
|
248
|
+
it("POSTs /start", async () => {
|
|
249
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
|
|
250
|
+
await run(["agents", "--company", "acme", "start", "agt_1"]);
|
|
251
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
252
|
+
expect(String(url)).toContain("/v1/agents/agt_1/start");
|
|
253
|
+
expect(init?.method).toBe("POST");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("POSTs /stop", async () => {
|
|
257
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
|
|
258
|
+
await run(["agents", "--company", "acme", "stop", "agt_1"]);
|
|
259
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/agents/agt_1/stop");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("POSTs /retry", async () => {
|
|
263
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { uid: "agt_1" }));
|
|
264
|
+
await run(["agents", "--company", "acme", "retry", "agt_1"]);
|
|
265
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/agents/agt_1/retry");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("hq agents rm", () => {
|
|
270
|
+
it("refuses without --yes and does not call the API", async () => {
|
|
271
|
+
await expect(
|
|
272
|
+
run(["agents", "--company", "acme", "rm", "agt_1"]),
|
|
273
|
+
).rejects.toThrow("process.exit(1)");
|
|
274
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("DELETEs the agent with --yes", async () => {
|
|
278
|
+
fetchSpy.mockResolvedValueOnce(
|
|
279
|
+
jsonResponse(200, { uid: "agt_1", terminal: true }),
|
|
280
|
+
);
|
|
281
|
+
await run(["agents", "--company", "acme", "rm", "agt_1", "--yes"]);
|
|
282
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
283
|
+
expect(String(url)).toContain("/v1/agents/agt_1");
|
|
284
|
+
expect(init?.method).toBe("DELETE");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("hq agents status", () => {
|
|
289
|
+
it("exits 1 on a non-2xx and surfaces the error", async () => {
|
|
290
|
+
fetchSpy.mockResolvedValueOnce(
|
|
291
|
+
jsonResponse(403, { error: "forbidden" }),
|
|
292
|
+
);
|
|
293
|
+
await expect(
|
|
294
|
+
run(["agents", "--company", "acme", "status", "agt_1"]),
|
|
295
|
+
).rejects.toThrow("process.exit(1)");
|
|
296
|
+
});
|
|
297
|
+
});
|