feat(gstack2): add capability readiness process

This commit is contained in:
Sinabina
2026-07-20 16:23:38 -07:00
parent a1b2b05a18
commit 2e1e52eae7
12 changed files with 324 additions and 8 deletions
+6 -2
View File
@@ -611,8 +611,12 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna
## Troubleshooting ## Troubleshooting
For GStack 2, first run `gstack doctor` (or `gstack doctor --json`) when the For GStack 2, run `gstack doctor --capability browser|design|diagram|pdf|ios`
optional runtime is installed. For skill discovery problems, use the standard (optionally `--json`) for a focused, non-mutating readiness result. It reports
pure-judgment availability, platform support, preview consent, install consent,
and runtime readiness separately; see
[`docs/gstack-2/CAPABILITY-READINESS.md`](docs/gstack-2/CAPABILITY-READINESS.md).
For skill discovery problems, use the standard
installer's list and reinstall commands; do not manually copy host paths. The installer's list and reinstall commands; do not manually copy host paths. The
entries below apply to legacy 1.x installations. entries below apply to legacy 1.x installations.
+75
View File
@@ -0,0 +1,75 @@
# Capability readiness
GStack's six public skills provide pure judgment without the optional runtime.
When a workflow needs executable evidence, inspect one of the five user-facing
capabilities without changing the machine:
```bash
gstack doctor --capability browser
gstack doctor --capability design --json
gstack doctor --capability diagram
gstack doctor --capability pdf
gstack doctor --capability ios
```
The focused result deliberately keeps five questions separate:
| Axis | Meaning |
|---|---|
| `judgment` | `available` even when no optional runtime exists. |
| `platform` | `supported` or `unsupported`; physical iOS is macOS-only. |
| `consent.preview` | Whether consent is required before an uncached public signed-manifest metadata request. Doctor never makes that request or grants consent. |
| `consent.install` | Separate consent required only after the complete preview. Doctor never installs or records consent. |
| `readiness` | Whether the selected local runtime capability is usable now. |
Readiness has exactly five states:
- `ready`: capability and managed-runtime checks passed.
- `degraded`: the capability passed, but the managed runtime reported a warning
such as recovery from an interrupted upgrade.
- `unavailable`: the capability is not selected, no runtime is active, or runtime
inspection could not establish availability.
- `unsupported`: the platform cannot provide the capability with GStack's
existing architecture.
- `failed`: an installed/selected capability failed its readiness evidence.
The JSON form follows the runtime's result convention: `ok` is true only for
`ready` and `degraded`; a non-ready focused doctor exits 1. Usage errors still
exit 2 through the standard CLI error envelope.
## Consent and setup flow
```text
pure judgment (always available)
|
v
capability doctor (local, non-mutating)
|
+-- ready/degraded --> capability-dependent work
|
+-- unsupported ----> judgment-only work or supported platform
|
+-- unavailable/failed
|
v
ask for preview consent
|
v
packaged bootstrap preview (no install)
|
v
ask for install consent
|
v
packaged bootstrap install
|
v
capability doctor again
```
Preview consent is not install consent. Deferring either does not block pure
judgment. Follow the packaged bootstrap contract for exact dependency closure,
compressed bytes, signature checks, and atomic installation. Do not run setup
from a standards-installed skill directory, add a browser provider, replace the
local Chromium/Playwright backend, or replace the physical-iOS
DebugBridge/CoreDevice harness.
+19 -6
View File
@@ -25,7 +25,13 @@ import {
runExternalEffect, runExternalEffect,
updateRunWorkflow, updateRunWorkflow,
} from "./state.js"; } from "./state.js";
import { runDoctor, formatDoctor } from "./doctor.js"; import {
CAPABILITY_READINESS_CAPABILITIES,
capabilityReadiness,
formatCapabilityReadiness,
runDoctor,
formatDoctor,
} from "./doctor.js";
import { cleanupRuntime } from "./cleanup.js"; import { cleanupRuntime } from "./cleanup.js";
import { import {
ContextClient, ContextClient,
@@ -157,11 +163,18 @@ async function initCommand({ args, home, cwd, stdout, legacyAlias = false }) {
} }
async function doctorCommand({ args, home, cwd, stdout }) { async function doctorCommand({ args, home, cwd, stdout }) {
const parsed = parseFlags(args, new Set(["--json", "--skill-api"])); const parsed = parseFlags(args, new Set(["--json", "--skill-api", "--capability"]));
if (parsed.positionals.length) throw cliError("Doctor accepts only named options", "USAGE"); if (parsed.positionals.length) throw cliError("Doctor accepts only named options", "USAGE");
const capability = parsed.values.get("--capability");
if (capability && !CAPABILITY_READINESS_CAPABILITIES.includes(capability)) {
throw cliError(`Unknown capability: ${capability}. Choose ${CAPABILITY_READINESS_CAPABILITIES.join(", ")}.`, "USAGE");
}
const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") }); const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") });
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(report, null, 2)}\n` : formatDoctor(report)); const result = capability ? capabilityReadiness(report, capability) : report;
return report.ok ? 0 : 1; write(stdout, parsed.flags.has("--json")
? `${JSON.stringify(result, null, 2)}\n`
: capability ? formatCapabilityReadiness(result) : formatDoctor(result));
return result.ok ? 0 : 1;
} }
async function configCommand({ args, home, cwd, stdout }) { async function configCommand({ args, home, cwd, stdout }) {
@@ -673,7 +686,7 @@ function parseFlags(args, allowed) {
continue; continue;
} }
if (!allowed.has(arg)) throw cliError(`Unknown option: ${arg}`, "USAGE"); if (!allowed.has(arg)) throw cliError(`Unknown option: ${arg}`, "USAGE");
if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api"].includes(arg)) { if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api", "--capability"].includes(arg)) {
const value = args[++index]; const value = args[++index];
if (value == null || value.startsWith("--")) throw cliError(`${arg} requires a value`, "USAGE"); if (value == null || value.startsWith("--")) throw cliError(`${arg} requires a value`, "USAGE");
values.set(arg, value); values.set(arg, value);
@@ -750,7 +763,7 @@ function usage() {
"Usage:\n" + "Usage:\n" +
" gstack init # initialize state only\n" + " gstack init # initialize state only\n" +
" gstack setup # compatibility alias for init\n" + " gstack setup # compatibility alias for init\n" +
" gstack doctor [--skill-api <version>] [--json]\n" + " gstack doctor [--capability browser|design|diagram|pdf|ios] [--skill-api <version>] [--json]\n" +
" gstack paths [--json|--shell]\n" + " gstack paths [--json|--shell]\n" +
" gstack runtime path <bundle-relative-path>\n" + " gstack runtime path <bundle-relative-path>\n" +
" gstack config get [key]\n" + " gstack config get [key]\n" +
+97
View File
@@ -17,6 +17,14 @@ import {
managedBunRelativePath, managedBunRelativePath,
} from "./install.js"; } from "./install.js";
export const CAPABILITY_READINESS_CAPABILITIES = Object.freeze([
"browser",
"design",
"diagram",
"pdf",
"ios",
]);
export async function runDoctor(options = {}) { export async function runDoctor(options = {}) {
const paths = resolveRuntimePaths(options); const paths = resolveRuntimePaths(options);
const checks = []; const checks = [];
@@ -183,6 +191,82 @@ export async function runDoctor(options = {}) {
}; };
} }
export function capabilityReadiness(report, capability, options = {}) {
if (!CAPABILITY_READINESS_CAPABILITIES.includes(capability)) {
throw new TypeError(`Unknown capability: ${capability}`);
}
const platform = options.platform ?? process.platform;
const checkedAt = report.checkedAt;
const judgment = {
status: "available",
message: "Pure-judgment skill guidance is available without the optional runtime.",
};
if (capability === "ios" && platform !== "darwin") {
return {
ok: false,
capability,
checkedAt,
judgment,
platform: { status: "unsupported", platform, message: "Physical iOS requires macOS and the existing CoreDevice harness." },
consent: {
preview: { status: "not-applicable", granted: false },
install: { status: "not-applicable", granted: false },
},
readiness: { status: "unsupported", message: "The runtime capability is unsupported on this platform." },
nextAction: "Continue with pure judgment or move the physical-iOS workflow to macOS.",
};
}
const capabilityCheck = report.checks.find((check) => check.id === `capability:${capability}`);
const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime");
let status;
if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready";
else if (capabilityCheck?.status === "pass") status = "degraded";
else if (capabilityCheck?.status === "fail") status = "failed";
else status = "unavailable";
const needsSetup = status === "unavailable" || status === "failed";
const messages = {
ready: "The selected capability passed its runtime readiness checks.",
degraded: "The capability check passed, but the managed runtime reported a degraded condition.",
unavailable: "The optional runtime capability is not installed or cannot currently be inspected.",
failed: "The selected capability is installed but failed readiness checks.",
};
return {
ok: status === "ready" || status === "degraded",
capability,
checkedAt,
judgment,
platform: { status: "supported", platform },
consent: {
preview: {
status: needsSetup ? "required" : "not-required",
granted: false,
message: needsSetup
? "Consent is required before an uncached signed-manifest metadata preview; this command does not grant it."
: "No setup preview is needed for the current readiness state.",
},
install: {
status: needsSetup ? "required-after-preview" : "not-required",
granted: false,
message: needsSetup
? "Install consent is separate and may be requested only after the complete preview; this command does not grant it."
: "No install is needed for the current readiness state.",
},
},
readiness: {
status,
message: messages[status],
evidence: [runtimeCheck, capabilityCheck].filter(Boolean),
},
nextAction: needsSetup
? `Ask for preview consent, then run the packaged bootstrap preview for ${capability}; continue judgment-only work if setup is deferred.`
: status === "degraded"
? "Review the managed-runtime warning before capability-dependent evidence work."
: "Proceed with capability-dependent work.",
};
}
async function inspectRuntimeBun(activeRoot, manifest) { async function inspectRuntimeBun(activeRoot, manifest) {
const relative = managedBunRelativePath(); const relative = managedBunRelativePath();
const declared = manifest?.tools?.bun; const declared = manifest?.tools?.bun;
@@ -328,3 +412,16 @@ export function formatDoctor(report) {
for (const check of report.checks) lines.push(`${symbol[check.status]} ${check.id}: ${check.message}`); for (const check of report.checks) lines.push(`${symbol[check.status]} ${check.id}: ${check.message}`);
return `${lines.join("\n")}\n`; return `${lines.join("\n")}\n`;
} }
export function formatCapabilityReadiness(report) {
const lines = [
`gstack capability readiness: ${report.capability}`,
`judgment: ${report.judgment.status}`,
`platform: ${report.platform.status}`,
`preview consent: ${report.consent.preview.status}`,
`install consent: ${report.consent.install.status}`,
`readiness: ${report.readiness.status}`,
`next: ${report.nextAction}`,
];
return `${lines.join("\n")}\n`;
}
+2
View File
@@ -516,6 +516,8 @@ The package/runtime compatibility tuple is \`schemaVersion=1\`, \`runtimeVersion
Every optional-runtime tool result must satisfy \`references/support/execution-result-contract.json\` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy \`references/support/execution-result-contract.json\` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use \`gstack doctor --capability browser|design|diagram|pdf|ios\` (optionally \`--json\`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly \`ready\`, \`degraded\`, \`unavailable\`, \`unsupported\`, or \`failed\`. Doctor never grants or persists consent, previews metadata, or installs anything. \`unavailable\` means setup may be offered; \`failed\` means selected runtime evidence failed; \`unsupported\` is a platform boundary; and \`degraded\` means the capability passed while the managed runtime has a warning.
The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes\`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes\`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
`; `;
} }
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+2
View File
@@ -23,4 +23,6 @@ The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.
Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code. Every optional-runtime tool result must satisfy `references/support/execution-result-contract.json` before it is presented as success. Success requires non-empty evidence. Empty or malformed output and explicit degraded, unsupported, or failed statuses remain non-success with stable codes; human renderings must preserve that status and code.
Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning.
The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment.
+113
View File
@@ -0,0 +1,113 @@
import { describe, expect, test } from "bun:test";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { main } from "../runtime/cli.js";
import { capabilityReadiness, formatCapabilityReadiness } from "../runtime/doctor.js";
import { setupRuntime } from "../runtime/setup.js";
function report(checks: Array<Record<string, unknown>>) {
return {
ok: !checks.some((check) => check.status === "fail"),
home: "/fixture/.gstack",
checkedAt: "2026-07-20T00:00:00.000Z",
checks,
};
}
function capture() {
let value = "";
return { stream: { write: (chunk: string) => { value += chunk; } }, value: () => value };
}
describe("capability readiness", () => {
test("keeps pure judgment, preview consent, and install consent separate when unavailable", () => {
const result = capabilityReadiness(report([
{ id: "managed-runtime", status: "fail", message: "No active managed runtime" },
{ id: "capability:pdf", status: "warn", message: "not installed" },
]), "pdf");
expect(result).toMatchObject({
ok: false,
judgment: { status: "available" },
platform: { status: "supported" },
consent: {
preview: { status: "required", granted: false },
install: { status: "required-after-preview", granted: false },
},
readiness: { status: "unavailable" },
});
});
test("distinguishes ready, degraded, and failed runtime states", () => {
const ready = capabilityReadiness(report([
{ id: "managed-runtime", status: "pass", message: "active" },
{ id: "capability:design", status: "pass", message: "runnable" },
]), "design");
const degraded = capabilityReadiness(report([
{ id: "managed-runtime", status: "warn", message: "recovered" },
{ id: "capability:design", status: "pass", message: "runnable" },
]), "design");
const failed = capabilityReadiness(report([
{ id: "managed-runtime", status: "pass", message: "active" },
{ id: "capability:diagram", status: "fail", message: "launcher metadata missing" },
]), "diagram");
expect(ready).toMatchObject({ ok: true, readiness: { status: "ready" } });
expect(degraded).toMatchObject({ ok: true, readiness: { status: "degraded" } });
expect(failed).toMatchObject({ ok: false, readiness: { status: "failed" } });
expect(failed.consent.install.status).toBe("required-after-preview");
});
test("reports physical iOS as unsupported without turning off pure judgment", () => {
const result = capabilityReadiness(report([]), "ios", { platform: "linux" });
expect(result).toMatchObject({
ok: false,
judgment: { status: "available" },
platform: { status: "unsupported", platform: "linux" },
consent: {
preview: { status: "not-applicable" },
install: { status: "not-applicable" },
},
readiness: { status: "unsupported" },
});
});
test("human output exposes every state axis", () => {
const result = capabilityReadiness(report([
{ id: "managed-runtime", status: "pass", message: "active" },
{ id: "capability:browser", status: "pass", message: "launched" },
]), "browser");
expect(formatCapabilityReadiness(result)).toContain("judgment: available");
expect(formatCapabilityReadiness(result)).toContain("preview consent: not-required");
expect(formatCapabilityReadiness(result)).toContain("install consent: not-required");
expect(formatCapabilityReadiness(result)).toContain("readiness: ready");
});
test("doctor capability JSON is non-mutating and uses the unified result envelope", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-capability-doctor-"));
const home = path.join(root, "home");
const stdout = capture();
const stderr = capture();
try {
await setupRuntime({ home, cwd: root });
const exit = await main(["doctor", "--capability", "pdf", "--json"], {
cwd: root,
env: { ...process.env, GSTACK_HOME: home },
stdout: stdout.stream,
stderr: stderr.stream,
});
const result = JSON.parse(stdout.value());
expect(exit).toBe(1);
expect(stderr.value()).toBe("");
expect(result).toMatchObject({
ok: false,
capability: "pdf",
judgment: { status: "available" },
readiness: { status: "unavailable" },
});
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});