agent-dealer 0.2.0 → 1.0.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/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import
|
|
3
|
-
import { claudeBinExists, cursorBinExists, resolveClaudeBin, cursorInvokeArgs, resolveCursorBin, } from "../cli-env.js";
|
|
4
|
-
import { checkAgentDeckHealth, isAgentDeckMcpRegistered } from "./agent-deck.js";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { CODEX_AUTH_REMEDIATION, cursorAuthIssueFromOutput, runtimeAuthIssueFromOutput, } from "@agent-dealer/shared";
|
|
3
|
+
import { claudeBinExists, cursorBinExists, resolveClaudeBin, cursorInvokeArgs, resolveCursorBin, resolveCodexBin, codexBinExists, } from "../cli-env.js";
|
|
4
|
+
import { checkAgentDeckHealth, fetchDecks, isAgentDeckMcpRegistered } from "./agent-deck.js";
|
|
5
|
+
import { runtimeAvailability } from "../repository/runtime-availability.js";
|
|
6
|
+
const RUNTIME_LABEL = {
|
|
7
|
+
claude_code: "Claude",
|
|
8
|
+
cursor_local: "Cursor",
|
|
9
|
+
codex_local: "Codex",
|
|
10
|
+
};
|
|
11
|
+
function capHealthIssues(runtime) {
|
|
12
|
+
const avail = runtimeAvailability(runtime);
|
|
13
|
+
if (avail.available)
|
|
14
|
+
return [];
|
|
15
|
+
const until = new Date(avail.until).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
code: "usage_capped",
|
|
19
|
+
message: `${RUNTIME_LABEL[runtime]} capped until ${until}`,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
5
23
|
const RUNTIME_CACHE_MS = 60_000;
|
|
6
24
|
const runtimeIssueCache = new Map();
|
|
25
|
+
let githubIssueCache = null;
|
|
26
|
+
/** Exported for tests — clears the shared github + runtime health caches. */
|
|
27
|
+
export function clearAgentHealthCaches() {
|
|
28
|
+
runtimeIssueCache.clear();
|
|
29
|
+
githubIssueCache = null;
|
|
30
|
+
}
|
|
7
31
|
function runCommand(cmd, args, timeoutMs = 8000) {
|
|
8
32
|
return new Promise((resolve) => {
|
|
9
33
|
const child = spawn(cmd, args, {
|
|
@@ -31,50 +55,216 @@ function runCommand(cmd, args, timeoutMs = 8000) {
|
|
|
31
55
|
});
|
|
32
56
|
});
|
|
33
57
|
}
|
|
34
|
-
|
|
58
|
+
/** Exported for direct testing — bypasses the 60s cache in runtimeIssues(). */
|
|
59
|
+
export async function runtimeIssuesUncached(runtime) {
|
|
60
|
+
const issues = [];
|
|
35
61
|
if (runtime === "claude_code") {
|
|
36
|
-
if (claudeBinExists())
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
62
|
+
if (!claudeBinExists()) {
|
|
63
|
+
const ver = await runCommand(resolveClaudeBin(), ["--version"]);
|
|
64
|
+
if (!ver.ok) {
|
|
65
|
+
issues.push({ code: "cli_missing", message: "Claude CLI not found — install Claude Code" });
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
41
68
|
}
|
|
42
|
-
|
|
69
|
+
// NOT-133: Claude had no auth preflight at all, so a logged-out Claude agent was admitted
|
|
70
|
+
// exactly the way a logged-out Cursor one was. `claude auth status` is local, offline and
|
|
71
|
+
// free (it reads the credential store and prints JSON) — unlike `claude -p`, which bills.
|
|
72
|
+
// Its output is what gets classified, not its exit code: logged out it prints
|
|
73
|
+
// `"loggedIn": false` *and* exits 1, so only the positive signal in the body blocks. An
|
|
74
|
+
// older CLI without the subcommand prints an unknown-command error that matches nothing,
|
|
75
|
+
// and must not become a false block.
|
|
76
|
+
if (!claudeUsesThirdPartyProvider()) {
|
|
77
|
+
const auth = await runCommand(resolveClaudeBin(), ["auth", "status"]);
|
|
78
|
+
const authIssue = runtimeAuthIssueFromOutput("claude_code", auth.output);
|
|
79
|
+
if (authIssue)
|
|
80
|
+
issues.push(authIssue);
|
|
81
|
+
}
|
|
82
|
+
return issues;
|
|
83
|
+
}
|
|
84
|
+
if (runtime === "codex_local") {
|
|
85
|
+
if (!codexBinExists()) {
|
|
86
|
+
const ver = await runCommand(resolveCodexBin(), ["--version"]);
|
|
87
|
+
if (!ver.ok) {
|
|
88
|
+
issues.push({ code: "cli_missing", message: "Codex CLI not found — install Codex (`codex`)" });
|
|
89
|
+
return issues;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// `codex --version` succeeds without auth — use login status for auth health.
|
|
93
|
+
const login = await runCommand(resolveCodexBin(), ["login", "status"]);
|
|
94
|
+
// The classifier runs first: `codex login status` prints "Not logged in" and the
|
|
95
|
+
// exit-code heuristic below would read that as "logged in" on its own, because the
|
|
96
|
+
// substring "logged in" is inside it.
|
|
97
|
+
const authIssue = runtimeAuthIssueFromOutput("codex_local", login.output);
|
|
98
|
+
if (authIssue) {
|
|
99
|
+
issues.push(authIssue);
|
|
100
|
+
return issues;
|
|
101
|
+
}
|
|
102
|
+
const out = login.output.toLowerCase();
|
|
103
|
+
const loggedIn = login.ok &&
|
|
104
|
+
(out.includes("logged in") || out.includes("authenticated") || out.includes("api key"));
|
|
105
|
+
if (!loggedIn) {
|
|
106
|
+
issues.push({ code: "runtime_auth", message: CODEX_AUTH_REMEDIATION });
|
|
107
|
+
}
|
|
108
|
+
return issues;
|
|
43
109
|
}
|
|
44
110
|
const status = await runCommand(resolveCursorBin(), cursorInvokeArgs(["status"]));
|
|
45
|
-
|
|
46
|
-
|
|
111
|
+
// A failed *spawn* resolves with the error message as its output (`spawn cursor-agent
|
|
112
|
+
// ENOENT`), not with empty output — so an absent binary must be recognised here or it
|
|
113
|
+
// falls through to the unconfirmed-auth branch below and names the wrong remedy.
|
|
114
|
+
if (!status.ok && (/\bENOENT\b/.test(status.output) || (!status.output.trim() && !cursorBinExists()))) {
|
|
115
|
+
issues.push({ code: "cli_missing", message: "cursor-agent not found — run: curl https://cursor.com/install -fsS | bash" });
|
|
116
|
+
return issues;
|
|
117
|
+
}
|
|
118
|
+
const authIssue = cursorAuthIssueFromOutput(status.output);
|
|
119
|
+
if (authIssue) {
|
|
120
|
+
issues.push(authIssue);
|
|
121
|
+
return issues;
|
|
47
122
|
}
|
|
123
|
+
// NOT-133: an unclassified *failure* of the probe itself (non-zero exit, timeout) used to
|
|
124
|
+
// be read as "healthy" and admitted the agent. Silence is not evidence of auth, so this
|
|
125
|
+
// fails closed: the agent stays unhealthy — and its issues stay queued — until the probe
|
|
126
|
+
// succeeds. That is a deliberate trade against the incident, where admitting on a guess
|
|
127
|
+
// cost 12 dead sessions and parked three issues on a human. Reported as runtime_auth so
|
|
128
|
+
// the existing agents-page CLI status renders it, with a message that says plainly the
|
|
129
|
+
// state is unconfirmed rather than asserting the agent is logged out.
|
|
130
|
+
if (!status.ok) {
|
|
131
|
+
const detail = status.output.trim().split("\n").slice(-1)[0] ?? "no output";
|
|
132
|
+
issues.push({
|
|
133
|
+
code: "runtime_auth",
|
|
134
|
+
message: `Could not confirm Cursor auth — \`cursor-agent status\` failed (${detail})`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return issues;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Bedrock/Vertex installs authenticate through AWS/GCP credentials instead of a Claude
|
|
141
|
+
* login, so `claude auth status` is not the authority on whether they can run.
|
|
142
|
+
*/
|
|
143
|
+
function claudeUsesThirdPartyProvider() {
|
|
144
|
+
return (process.env.CLAUDE_CODE_USE_BEDROCK === "1" || process.env.CLAUDE_CODE_USE_VERTEX === "1");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Issue workflows always use `gh` after a developer session (draft PR + checks). A bad
|
|
148
|
+
* or missing GitHub CLI auth wastes a full agent run and lands as `adapter_failure` —
|
|
149
|
+
* surface it on every agent so Start / intake can refuse before that spend.
|
|
150
|
+
*/
|
|
151
|
+
export async function githubIssuesUncached() {
|
|
152
|
+
const status = await runCommand("gh", ["auth", "status"]);
|
|
48
153
|
const out = status.output.toLowerCase();
|
|
49
|
-
if (out.includes("
|
|
50
|
-
|
|
154
|
+
if (out.includes("enoent") ||
|
|
155
|
+
out.includes("not found") ||
|
|
156
|
+
out.includes("no such file") ||
|
|
157
|
+
(out.includes("spawn") && out.includes("gh"))) {
|
|
158
|
+
return [{ code: "github_cli_missing", message: "gh CLI not found — install GitHub CLI (`gh`)" }];
|
|
159
|
+
}
|
|
160
|
+
if (!status.ok ||
|
|
161
|
+
out.includes("not logged in") ||
|
|
162
|
+
out.includes("failed to log in") ||
|
|
163
|
+
out.includes("token in keyring is invalid") ||
|
|
164
|
+
out.includes("re-authenticate") ||
|
|
165
|
+
out.includes("to re-authenticate")) {
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
code: "github_auth",
|
|
169
|
+
message: "Run `gh auth login` — GitHub CLI auth required to open PRs",
|
|
170
|
+
},
|
|
171
|
+
];
|
|
51
172
|
}
|
|
52
173
|
return [];
|
|
53
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Unit/CI tests must not call the real `gh auth status` — runners usually have no
|
|
177
|
+
* usable token, and the startWorkflow preflight would refuse every kick. Set
|
|
178
|
+
* `AGENT_DEALER_SKIP_GITHUB_HEALTH=1` in `test:unit` / `test:ci`. Production and
|
|
179
|
+
* local `npm run dev` leave it unset so Start still refuses bad auth.
|
|
180
|
+
*/
|
|
181
|
+
function githubHealthSkippedForTests() {
|
|
182
|
+
return process.env.AGENT_DEALER_SKIP_GITHUB_HEALTH === "1";
|
|
183
|
+
}
|
|
184
|
+
/** Synchronous for startWorkflow — issue kick must refuse before spending a developer round. */
|
|
185
|
+
export function githubIssuesSync() {
|
|
186
|
+
if (githubHealthSkippedForTests())
|
|
187
|
+
return [];
|
|
188
|
+
if (githubIssueCache && Date.now() - githubIssueCache.at < RUNTIME_CACHE_MS) {
|
|
189
|
+
return githubIssueCache.issues;
|
|
190
|
+
}
|
|
191
|
+
const result = spawnSync("gh", ["auth", "status"], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
timeout: 8000,
|
|
194
|
+
env: process.env,
|
|
195
|
+
});
|
|
196
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}${result.error?.message ?? ""}`;
|
|
197
|
+
const out = output.toLowerCase();
|
|
198
|
+
let issues = [];
|
|
199
|
+
if (result.error ||
|
|
200
|
+
out.includes("enoent") ||
|
|
201
|
+
out.includes("not found") ||
|
|
202
|
+
out.includes("no such file")) {
|
|
203
|
+
issues = [{ code: "github_cli_missing", message: "gh CLI not found — install GitHub CLI (`gh`)" }];
|
|
204
|
+
}
|
|
205
|
+
else if (result.status !== 0 ||
|
|
206
|
+
out.includes("not logged in") ||
|
|
207
|
+
out.includes("failed to log in") ||
|
|
208
|
+
out.includes("token in keyring is invalid") ||
|
|
209
|
+
out.includes("re-authenticate") ||
|
|
210
|
+
out.includes("to re-authenticate")) {
|
|
211
|
+
issues = [
|
|
212
|
+
{
|
|
213
|
+
code: "github_auth",
|
|
214
|
+
message: "Run `gh auth login` — GitHub CLI auth required to open PRs",
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
githubIssueCache = { at: Date.now(), issues };
|
|
219
|
+
return issues;
|
|
220
|
+
}
|
|
221
|
+
async function githubIssues() {
|
|
222
|
+
if (githubIssueCache && Date.now() - githubIssueCache.at < RUNTIME_CACHE_MS) {
|
|
223
|
+
return githubIssueCache.issues;
|
|
224
|
+
}
|
|
225
|
+
const issues = await githubIssuesUncached();
|
|
226
|
+
githubIssueCache = { at: Date.now(), issues };
|
|
227
|
+
return issues;
|
|
228
|
+
}
|
|
54
229
|
async function runtimeIssues(runtime) {
|
|
230
|
+
const capIssues = capHealthIssues(runtime);
|
|
55
231
|
const cached = runtimeIssueCache.get(runtime);
|
|
56
232
|
if (cached && Date.now() - cached.at < RUNTIME_CACHE_MS) {
|
|
57
|
-
return cached.issues;
|
|
233
|
+
return [...capIssues, ...cached.issues];
|
|
58
234
|
}
|
|
59
235
|
const issues = await runtimeIssuesUncached(runtime);
|
|
60
|
-
|
|
61
|
-
|
|
236
|
+
const nonCap = issues.filter((i) => i.code !== "usage_capped");
|
|
237
|
+
runtimeIssueCache.set(runtime, { at: Date.now(), issues: nonCap });
|
|
238
|
+
return [...capIssues, ...nonCap];
|
|
62
239
|
}
|
|
63
|
-
function agentSpecificIssues(agent, agentDeckOnline, mcpRegistered) {
|
|
240
|
+
function agentSpecificIssues(agent, agentDeckOnline, mcpRegistered, deckAccessResult) {
|
|
64
241
|
const issues = [];
|
|
65
|
-
if (!agent.
|
|
66
|
-
issues.push({ code: "workspace_missing", message: "Set workspace on Agents page" });
|
|
67
|
-
}
|
|
68
|
-
else if (!fs.existsSync(agent.workspaceRoot)) {
|
|
242
|
+
if (!agent.deckId) {
|
|
69
243
|
issues.push({
|
|
70
|
-
code: "
|
|
71
|
-
message:
|
|
244
|
+
code: "deck_missing",
|
|
245
|
+
message: "Set an Agent Deck on the Agents page — workers never start without one",
|
|
72
246
|
});
|
|
247
|
+
return issues;
|
|
73
248
|
}
|
|
74
|
-
if (
|
|
249
|
+
if (!agentDeckOnline) {
|
|
75
250
|
issues.push({ code: "deck_offline", message: "Agent Deck offline — deck MCP unavailable" });
|
|
76
251
|
}
|
|
77
|
-
|
|
252
|
+
// resolveDeckName silently returns null on this same failure elsewhere (route/index.ts) —
|
|
253
|
+
// surface it here so a bound deck that can no longer be read isn't just a quiet no-op. A
|
|
254
|
+
// *successful* metadata call that simply doesn't include this deck (deleted) is the same
|
|
255
|
+
// user-visible failure as the call itself failing.
|
|
256
|
+
if (agentDeckOnline && deckAccessResult) {
|
|
257
|
+
if (!deckAccessResult.ok) {
|
|
258
|
+
issues.push({ code: "deck_unauthorized", message: deckAccessResult.message });
|
|
259
|
+
}
|
|
260
|
+
else if (!deckAccessResult.decks.some((d) => d.id === agent.deckId)) {
|
|
261
|
+
issues.push({
|
|
262
|
+
code: "deck_unauthorized",
|
|
263
|
+
message: `Deck ${agent.deckName ?? agent.deckId} is not available from Agent Deck (deleted or unreachable)`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (agent.runtime === "claude_code" && agentDeckOnline && !mcpRegistered) {
|
|
78
268
|
issues.push({
|
|
79
269
|
code: "mcp_not_registered",
|
|
80
270
|
message: "Run agent-deck setup --client claude --start (Claude MCP not registered)",
|
|
@@ -82,14 +272,16 @@ function agentSpecificIssues(agent, agentDeckOnline, mcpRegistered) {
|
|
|
82
272
|
}
|
|
83
273
|
return issues;
|
|
84
274
|
}
|
|
85
|
-
export async function healthForAgent(agent, agentDeckOnline, runtimeIssuesByRuntime, mcpRegistered) {
|
|
275
|
+
export async function healthForAgent(agent, agentDeckOnline, runtimeIssuesByRuntime, mcpRegistered, deckAccessResult = null, githubIssuesList) {
|
|
86
276
|
const runtime = runtimeIssuesByRuntime !== undefined
|
|
87
277
|
? (runtimeIssuesByRuntime.get(agent.runtime) ?? [])
|
|
88
278
|
: await runtimeIssues(agent.runtime);
|
|
89
279
|
const deckMcpOk = mcpRegistered ?? isAgentDeckMcpRegistered();
|
|
280
|
+
const github = githubIssuesList ?? (await githubIssues());
|
|
90
281
|
const issues = [
|
|
91
282
|
...runtime,
|
|
92
|
-
...
|
|
283
|
+
...github,
|
|
284
|
+
...agentSpecificIssues(agent, agentDeckOnline, deckMcpOk, deckAccessResult),
|
|
93
285
|
];
|
|
94
286
|
return {
|
|
95
287
|
...agent,
|
|
@@ -100,10 +292,15 @@ export async function healthForAgent(agent, agentDeckOnline, runtimeIssuesByRunt
|
|
|
100
292
|
export async function listAgentsWithHealth(agents) {
|
|
101
293
|
const agentDeckOnline = await checkAgentDeckHealth();
|
|
102
294
|
const mcpRegistered = isAgentDeckMcpRegistered();
|
|
295
|
+
const needsDeckAccess = agents.some((a) => a.deckId);
|
|
296
|
+
const deckAccessResult = agentDeckOnline && needsDeckAccess ? await fetchDecks() : null;
|
|
103
297
|
const runtimes = [...new Set(agents.map((a) => a.runtime))];
|
|
104
298
|
const runtimeIssuesByRuntime = new Map();
|
|
105
|
-
await Promise.all(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
299
|
+
const [, githubIssuesList] = await Promise.all([
|
|
300
|
+
Promise.all(runtimes.map(async (runtime) => {
|
|
301
|
+
runtimeIssuesByRuntime.set(runtime, await runtimeIssues(runtime));
|
|
302
|
+
})),
|
|
303
|
+
githubIssues(),
|
|
304
|
+
]);
|
|
305
|
+
return Promise.all(agents.map((a) => healthForAgent(a, agentDeckOnline, runtimeIssuesByRuntime, mcpRegistered, deckAccessResult, githubIssuesList)));
|
|
109
306
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// packages/server/src/adapters/agent-health.test.ts
|
|
2
|
+
//
|
|
3
|
+
// An agent bound to a deck that Agent Deck is reachable for (agentDeckOnline) but whose
|
|
4
|
+
// launch deck-metadata call fails — or succeeds but simply no longer includes this deck
|
|
5
|
+
// (deleted) — must surface a distinct issue. resolveDeckName silently swallows both of
|
|
6
|
+
// the same failures to null elsewhere, so this is the one place an operator can see why
|
|
7
|
+
// deck resolution is broken.
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-agenthealth-"));
|
|
16
|
+
const { migrate, getDb } = await import("../db/index.js");
|
|
17
|
+
const { createAgent, getAgent } = await import("../repository/agents.js");
|
|
18
|
+
const { healthForAgent, runtimeIssuesUncached, githubIssuesUncached, clearAgentHealthCaches } = await import("./agent-health.js");
|
|
19
|
+
migrate();
|
|
20
|
+
const FAILURE = { ok: false, code: "DECK_UNAVAILABLE", message: "Agent Deck API error: 502" };
|
|
21
|
+
/** Tests inject an empty github list so host `gh auth` does not pollute assertions. */
|
|
22
|
+
const NO_GITHUB = [];
|
|
23
|
+
test("missing deckId: reports deck_missing", async () => {
|
|
24
|
+
const created = createAgent({
|
|
25
|
+
name: "no-deck",
|
|
26
|
+
runtime: "claude_code",
|
|
27
|
+
deckId: "00000000-0000-4000-a000-000000000099",
|
|
28
|
+
});
|
|
29
|
+
getDb().prepare("UPDATE agents SET deck_id = NULL WHERE id = ?").run(created.id);
|
|
30
|
+
const agent = getAgent(created.id);
|
|
31
|
+
const result = await healthForAgent(agent, true, new Map(), true, FAILURE, NO_GITHUB);
|
|
32
|
+
assert.equal(result.issues.some((i) => i.code === "deck_missing"), true);
|
|
33
|
+
assert.equal(result.healthy, false);
|
|
34
|
+
});
|
|
35
|
+
test("agent deck offline: reports deck_offline, not deck_unauthorized", async () => {
|
|
36
|
+
const agent = createAgent({
|
|
37
|
+
name: "offline",
|
|
38
|
+
runtime: "claude_code",
|
|
39
|
+
deckId: randomUUID(),
|
|
40
|
+
});
|
|
41
|
+
const result = await healthForAgent(agent, false, new Map(), true, FAILURE, NO_GITHUB);
|
|
42
|
+
assert.deepEqual(result.issues.map((i) => i.code).sort(), ["deck_offline"]);
|
|
43
|
+
});
|
|
44
|
+
test("agent deck online but metadata unavailable: reports deck_unauthorized with the Deck message", async () => {
|
|
45
|
+
const agent = createAgent({
|
|
46
|
+
name: "unavailable",
|
|
47
|
+
runtime: "claude_code",
|
|
48
|
+
deckId: randomUUID(),
|
|
49
|
+
});
|
|
50
|
+
const result = await healthForAgent(agent, true, new Map(), true, FAILURE, NO_GITHUB);
|
|
51
|
+
const issue = result.issues.find((i) => i.code === "deck_unauthorized");
|
|
52
|
+
assert.ok(issue, "expected a deck_unauthorized issue");
|
|
53
|
+
assert.equal(issue?.message, "Agent Deck API error: 502");
|
|
54
|
+
});
|
|
55
|
+
test("agent deck online, metadata call succeeds, but this deck isn't in the returned set: reports deck_unauthorized", async () => {
|
|
56
|
+
const deckId = randomUUID();
|
|
57
|
+
const agent = createAgent({ name: "stale-deck", runtime: "claude_code", deckId });
|
|
58
|
+
const deckAccessResult = { ok: true, decks: [{ id: randomUUID(), name: "some-other-deck" }] };
|
|
59
|
+
const result = await healthForAgent(agent, true, new Map(), true, deckAccessResult, NO_GITHUB);
|
|
60
|
+
const issue = result.issues.find((i) => i.code === "deck_unauthorized");
|
|
61
|
+
assert.ok(issue, "expected a deck_unauthorized issue for a deck missing from the launch set");
|
|
62
|
+
});
|
|
63
|
+
test("agent deck online and this deck is in the returned set: healthy on the deck axis", async () => {
|
|
64
|
+
const deckId = randomUUID();
|
|
65
|
+
const agent = createAgent({ name: "healthy", runtime: "claude_code", deckId });
|
|
66
|
+
const deckAccessResult = { ok: true, decks: [{ id: deckId, name: "healthy-deck" }] };
|
|
67
|
+
const result = await healthForAgent(agent, true, new Map(), true, deckAccessResult, NO_GITHUB);
|
|
68
|
+
assert.equal(result.issues.some((i) => i.code === "deck_unauthorized" || i.code === "deck_offline"), false);
|
|
69
|
+
});
|
|
70
|
+
test("agent deck online with no deck-access result computed (e.g. no agent needed it): no false positive", async () => {
|
|
71
|
+
const agent = createAgent({
|
|
72
|
+
name: "no-result",
|
|
73
|
+
runtime: "claude_code",
|
|
74
|
+
deckId: randomUUID(),
|
|
75
|
+
});
|
|
76
|
+
const result = await healthForAgent(agent, true, new Map(), true, null, NO_GITHUB);
|
|
77
|
+
assert.equal(result.issues.some((i) => i.code === "deck_unauthorized" || i.code === "deck_offline"), false);
|
|
78
|
+
});
|
|
79
|
+
test("codex_local with a missing CLI reports cli_missing exactly once, not twice", async () => {
|
|
80
|
+
const prevCodexCli = process.env.CODEX_CLI;
|
|
81
|
+
process.env.CODEX_CLI = "/nonexistent/path/codex-does-not-exist";
|
|
82
|
+
try {
|
|
83
|
+
const issues = await runtimeIssuesUncached("codex_local");
|
|
84
|
+
assert.deepEqual(issues.map((i) => i.code), ["cli_missing"]);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (prevCodexCli === undefined)
|
|
88
|
+
delete process.env.CODEX_CLI;
|
|
89
|
+
else
|
|
90
|
+
process.env.CODEX_CLI = prevCodexCli;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
test("cursor_local with a bound deck: no deck access issue (launch MCP is supported)", async () => {
|
|
94
|
+
const agent = createAgent({
|
|
95
|
+
name: "cursor-with-deck",
|
|
96
|
+
runtime: "cursor_local",
|
|
97
|
+
deckId: randomUUID(),
|
|
98
|
+
});
|
|
99
|
+
const result = await healthForAgent(agent, true, new Map(), true, null, NO_GITHUB);
|
|
100
|
+
assert.equal(result.issues.some((i) => i.code === "deck_unauthorized" || i.code === "deck_offline"), false);
|
|
101
|
+
});
|
|
102
|
+
test("github_auth issues mark the agent unhealthy so Start can refuse before a wasted run", async () => {
|
|
103
|
+
const agent = createAgent({
|
|
104
|
+
name: "needs-gh",
|
|
105
|
+
runtime: "claude_code",
|
|
106
|
+
deckId: "00000000-0000-4000-a000-000000000099",
|
|
107
|
+
});
|
|
108
|
+
const gh = [
|
|
109
|
+
{ code: "github_auth", message: "Run `gh auth login` — GitHub CLI auth required to open PRs" },
|
|
110
|
+
];
|
|
111
|
+
const result = await healthForAgent(agent, true, new Map(), true, null, gh);
|
|
112
|
+
assert.equal(result.healthy, false);
|
|
113
|
+
assert.equal(result.issues.some((i) => i.code === "github_auth"), true);
|
|
114
|
+
});
|
|
115
|
+
test("githubIssuesUncached reports github_auth when gh auth status fails with an invalid token", async () => {
|
|
116
|
+
clearAgentHealthCaches();
|
|
117
|
+
// Exercise the real parser path by stubbing via PATH... we instead unit-test the
|
|
118
|
+
// injected-list path above for healthForAgent. Here we assert the uncached helper
|
|
119
|
+
// returns a known shape when `gh` is present but auth is bad — skip if gh missing.
|
|
120
|
+
const { spawnSync } = await import("node:child_process");
|
|
121
|
+
const probe = spawnSync("gh", ["--version"], { encoding: "utf8" });
|
|
122
|
+
if (probe.error || probe.status !== 0) {
|
|
123
|
+
// No gh on this host — nothing to assert about auth status.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const issues = await githubIssuesUncached();
|
|
127
|
+
for (const issue of issues) {
|
|
128
|
+
assert.ok(issue.code === "github_auth" || issue.code === "github_cli_missing");
|
|
129
|
+
assert.ok(issue.message.length > 0);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
test("cursor_keychain status/stderr fixture maps to a blocking health issue via cursorAuthIssueFromOutput", async () => {
|
|
133
|
+
const { cursorAuthIssueFromOutput } = await import("@agent-dealer/shared");
|
|
134
|
+
const fixture = `Cursor couldn't save your login to the macOS keychain (errSecDuplicateItem, security exit code 45).
|
|
135
|
+
The keychain item is stuck. Delete it and sign in again:
|
|
136
|
+
security delete-generic-password -s cursor-access-token -a cursor-user
|
|
137
|
+
agent login
|
|
138
|
+
`;
|
|
139
|
+
const issue = cursorAuthIssueFromOutput(fixture);
|
|
140
|
+
assert.equal(issue?.code, "cursor_keychain");
|
|
141
|
+
assert.match(issue.message, /delete-generic-password/);
|
|
142
|
+
const agent = createAgent({
|
|
143
|
+
name: "cursor-keychain",
|
|
144
|
+
runtime: "cursor_local",
|
|
145
|
+
deckId: "00000000-0000-4000-a000-000000000099",
|
|
146
|
+
});
|
|
147
|
+
const result = await healthForAgent(agent, true, new Map([["cursor_local", [issue]]]), true, null, NO_GITHUB);
|
|
148
|
+
assert.equal(result.healthy, false);
|
|
149
|
+
assert.equal(result.issues.some((i) => i.code === "cursor_keychain"), true);
|
|
150
|
+
});
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// NOT-133: the admission preflight classifies each runtime's *captured* logged-out
|
|
153
|
+
// output. Stubs replay the fixtures in packages/shared/src/fixtures/runtime-auth/.
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../shared/src/fixtures/runtime-auth");
|
|
156
|
+
/** A CLI stub that prints a captured fixture verbatim and exits with the captured code. */
|
|
157
|
+
function stubCli(name, fixture, exitCode) {
|
|
158
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cli-stub-"));
|
|
159
|
+
const bin = path.join(dir, name);
|
|
160
|
+
fs.writeFileSync(bin, `#!/bin/sh\ncat ${JSON.stringify(path.join(FIXTURES, fixture))}\nexit ${exitCode}\n`);
|
|
161
|
+
fs.chmodSync(bin, 0o755);
|
|
162
|
+
return bin;
|
|
163
|
+
}
|
|
164
|
+
async function withEnv(key, value, fn) {
|
|
165
|
+
const prev = process.env[key];
|
|
166
|
+
process.env[key] = value;
|
|
167
|
+
clearAgentHealthCaches();
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (prev === undefined)
|
|
173
|
+
delete process.env[key];
|
|
174
|
+
else
|
|
175
|
+
process.env[key] = prev;
|
|
176
|
+
clearAgentHealthCaches();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
test("logged-out `cursor-agent status` capture blocks the cursor runtime (exit 0 — output is the only signal)", async () => {
|
|
180
|
+
const issues = await withEnv("CURSOR_CLI", stubCli("cursor-agent", "cursor-agent-status-logged-out.txt", 0), () => runtimeIssuesUncached("cursor_local"));
|
|
181
|
+
assert.deepEqual(issues.map((i) => i.code), ["runtime_auth"]);
|
|
182
|
+
assert.match(issues[0].message, /cursor-agent login/);
|
|
183
|
+
assert.match(issues[0].message, /CURSOR_API_KEY/);
|
|
184
|
+
});
|
|
185
|
+
test("logged-in `cursor-agent status` capture leaves the cursor runtime healthy", async () => {
|
|
186
|
+
const issues = await withEnv("CURSOR_CLI", stubCli("cursor-agent", "cursor-agent-status-logged-in.txt", 0), () => runtimeIssuesUncached("cursor_local"));
|
|
187
|
+
assert.deepEqual(issues, []);
|
|
188
|
+
});
|
|
189
|
+
test("a cursor status probe that fails without a classifiable reason blocks rather than admits", async () => {
|
|
190
|
+
// NOT-133: silence used to be read as health, so an agent that could not be checked was
|
|
191
|
+
// admitted anyway. Unknown auth must wait, not spend infra attempts on ~1s dead sessions.
|
|
192
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cli-stub-"));
|
|
193
|
+
const bin = path.join(dir, "cursor-agent");
|
|
194
|
+
fs.writeFileSync(bin, "#!/bin/sh\necho 'panic: runtime broke' >&2\nexit 7\n");
|
|
195
|
+
fs.chmodSync(bin, 0o755);
|
|
196
|
+
const issues = await withEnv("CURSOR_CLI", bin, () => runtimeIssuesUncached("cursor_local"));
|
|
197
|
+
assert.deepEqual(issues.map((i) => i.code), ["runtime_auth"]);
|
|
198
|
+
assert.match(issues[0].message, /Could not confirm Cursor auth/);
|
|
199
|
+
});
|
|
200
|
+
test("logged-out `codex login status` capture blocks the codex runtime", async () => {
|
|
201
|
+
const issues = await withEnv("CODEX_CLI", stubCli("codex", "codex-login-status-logged-out.txt", 1), () => runtimeIssuesUncached("codex_local"));
|
|
202
|
+
assert.deepEqual(issues.map((i) => i.code), ["runtime_auth"]);
|
|
203
|
+
assert.match(issues[0].message, /codex login/);
|
|
204
|
+
});
|
|
205
|
+
test("logged-in `codex login status` capture leaves the codex runtime healthy", async () => {
|
|
206
|
+
const issues = await withEnv("CODEX_CLI", stubCli("codex", "codex-login-status-logged-in.txt", 0), () => runtimeIssuesUncached("codex_local"));
|
|
207
|
+
assert.deepEqual(issues, []);
|
|
208
|
+
});
|
|
209
|
+
test("a cursor probe that cannot spawn reports the missing CLI, not unconfirmed auth", async () => {
|
|
210
|
+
// A spawn failure resolves with `spawn <bin> ENOENT` as its *output*, so the
|
|
211
|
+
// empty-output test for a missing binary never fires and the operator would be told to
|
|
212
|
+
// check their login when the binary is what is absent.
|
|
213
|
+
const missing = path.join(os.tmpdir(), `dealer-absent-cursor-agent-${randomUUID()}`);
|
|
214
|
+
const issues = await withEnv("CURSOR_CLI", missing, () => runtimeIssuesUncached("cursor_local"));
|
|
215
|
+
assert.deepEqual(issues.map((i) => i.code), ["cli_missing"]);
|
|
216
|
+
assert.match(issues[0].message, /cursor\.com\/install/);
|
|
217
|
+
});
|
|
218
|
+
test("logged-out `claude auth status` capture blocks the claude runtime", async () => {
|
|
219
|
+
// Claude had no auth preflight at all before NOT-133 — a logged-out Claude agent was
|
|
220
|
+
// admitted exactly the way the logged-out Cursor ones were. Exit 1 with a JSON body: the
|
|
221
|
+
// classifier reads `"loggedIn": false`, never the status code.
|
|
222
|
+
const issues = await withEnv("CLAUDE_CLI", stubCli("claude", "claude-auth-status-logged-out.txt", 1), () => runtimeIssuesUncached("claude_code"));
|
|
223
|
+
assert.deepEqual(issues.map((i) => i.code), ["runtime_auth"]);
|
|
224
|
+
assert.match(issues[0].message, /claude auth login/);
|
|
225
|
+
});
|
|
226
|
+
test("logged-in `claude auth status` capture leaves the claude runtime healthy", async () => {
|
|
227
|
+
const issues = await withEnv("CLAUDE_CLI", stubCli("claude", "claude-auth-status-logged-in.txt", 0), () => runtimeIssuesUncached("claude_code"));
|
|
228
|
+
assert.deepEqual(issues, []);
|
|
229
|
+
});
|
|
230
|
+
test("an older claude CLI without `auth status` is not a false block", async () => {
|
|
231
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cli-stub-"));
|
|
232
|
+
const bin = path.join(dir, "claude");
|
|
233
|
+
fs.writeFileSync(bin, "#!/bin/sh\necho \"error: unknown command 'auth'\" >&2\nexit 1\n");
|
|
234
|
+
fs.chmodSync(bin, 0o755);
|
|
235
|
+
const issues = await withEnv("CLAUDE_CLI", bin, () => runtimeIssuesUncached("claude_code"));
|
|
236
|
+
assert.deepEqual(issues, []);
|
|
237
|
+
});
|