lllin000_PaperForge/paperforge/plugin/tests/capability-state.test.ts
LLLin000 0859652396 feat(#81): Release N+1 owner cutover — Python 3.11+ gate, fail-closed dispatch
- managed-runtime: collapse MIN_PYTHON_LEGACY/MIN_PYTHON_NEW → MIN_PYTHON (3.11)
- managed-runtime: remove PYTHON_310_DEPRECATED warning (3.10 now needs_repair)
- managed-runtime: single 3.11+ hard gate for all installations
- settings: cut getCachedPython fallback in _resolveRuntimeCommand → fail-closed
- settings: update Python version notices 3.10→3.11
- i18n: update user-facing Python version messages 3.10→3.11
- Python backend: checker.py, setup_wizard.py, status.py → 3.11 minimum
- Test updates: legacy fallback tests now verify fail-closed, 3.10 gating removed

Build: 264.4KB | Tests: 384/384 passed | Typecheck: clean
2026-07-19 14:32:12 +08:00

1110 lines
33 KiB
TypeScript

/**
* Focused tests for capability-state types, envelope validation, helpers,
* and the six-module control center rendering.
*/
import { describe, expect, it } from "vitest";
import {
isValidEnvelope,
createUnknownEnvelope,
createStaleEnvelope,
createInvalidEnvelope,
isEnvelopeStale,
isReadyEnvelope,
classifyCapabilityAction,
computeModuleSummary,
validatePersistedEnvelopes,
CAPABILITY_MODULES,
SCHEMA_VERSION,
ProbeEnvelope,
ActionPrimary,
probeAction,
setupAction,
} from "../src/constants";
// ── Helpers ──
/** Minimal valid envelope matching backend int/schema. */
function validEnvelope(
overrides: Partial<Record<string, unknown>> = {}
): Record<string, unknown> {
return {
schema_version: 1,
module: "installation",
capability_state: "ready",
severity: "ok",
activity_state: "idle",
activity_label: null,
activity_progress: null,
notices: [],
reason: { code: "ok", text: "All good" },
action: { primary: null },
updated_at: "2026-01-15T00:00:00.000Z",
ttl_seconds: 3600,
...overrides,
};
}
/** Full setup action from backend. */
const FULL_SETUP_ACTION: ActionPrimary = {
verb: "setup",
label: "Open Setup Wizard",
destructive: false,
destructive_scope: null,
destructive_effect: null,
confirmation_required: false,
confirmation_prompt: null,
command: "setup",
scope: "installation",
scope_count: 0,
};
// ── 1. Schema validation ──
describe("isValidEnvelope", () => {
it("passes for a valid envelope", () => {
expect(isValidEnvelope(validEnvelope())).toBe(true);
});
it("passes with backend-typical null activity_label and null activity_progress", () => {
expect(
isValidEnvelope(
validEnvelope({
activity_label: null,
activity_progress: null,
})
)
).toBe(true);
});
it("passes with full setup action primary", () => {
expect(
isValidEnvelope(
validEnvelope({
action: { primary: FULL_SETUP_ACTION },
})
)
).toBe(true);
});
it("rejects null input", () => {
expect(isValidEnvelope(null)).toBe(false);
});
it("rejects undefined input", () => {
expect(isValidEnvelope(undefined)).toBe(false);
});
it("rejects non-object input", () => {
expect(isValidEnvelope("string")).toBe(false);
expect(isValidEnvelope(42)).toBe(false);
});
it("rejects wrong schema_version", () => {
expect(isValidEnvelope(validEnvelope({ schema_version: "v1" }))).toBe(
false
);
expect(isValidEnvelope(validEnvelope({ schema_version: 2 }))).toBe(false);
});
it("rejects unknown module", () => {
expect(isValidEnvelope(validEnvelope({ module: "bogus" }))).toBe(false);
});
it("rejects module mismatch", () => {
expect(
isValidEnvelope(validEnvelope({ module: "help" }), "installation")
).toBe(false);
});
it("accepts matching module", () => {
expect(isValidEnvelope(validEnvelope({ module: "help" }), "help")).toBe(
true
);
});
it("accepts all six capability_states", () => {
for (const s of [
"unknown",
"unavailable",
"missing_input",
"needs_action",
"limited",
"ready",
]) {
expect(isValidEnvelope(validEnvelope({ capability_state: s }))).toBe(
true
);
}
});
it("rejects invalid capability_state", () => {
expect(isValidEnvelope(validEnvelope({ capability_state: "bogus" }))).toBe(
false
);
});
it("accepts all four severities", () => {
for (const s of ["unknown", "ok", "warning", "error"]) {
expect(isValidEnvelope(validEnvelope({ severity: s }))).toBe(true);
}
});
it("rejects invalid severity", () => {
expect(isValidEnvelope(validEnvelope({ severity: "critical" }))).toBe(
false
);
});
it("rejects missing activity_state", () => {
const { activity_state, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects missing activity_label", () => {
const { activity_label, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects missing activity_progress", () => {
const { activity_progress, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("accepts valid activity_progress object", () => {
expect(
isValidEnvelope(
validEnvelope({
activity_state: "running",
activity_label: "Probing...",
activity_progress: { current: 1, total: 5 },
})
)
).toBe(true);
});
it("rejects activity_progress with string current", () => {
expect(
isValidEnvelope(
validEnvelope({
activity_progress: { current: "1", total: 3 },
})
)
).toBe(false);
});
it("accepts notices array", () => {
expect(
isValidEnvelope(
validEnvelope({
notices: [{ level: "info", message: "test" }],
})
)
).toBe(true);
});
it("rejects missing notices", () => {
const { notices, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects missing reason", () => {
const { reason, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects reason missing code", () => {
expect(isValidEnvelope(validEnvelope({ reason: { text: "nope" } }))).toBe(
false
);
});
it("rejects reason missing text", () => {
expect(isValidEnvelope(validEnvelope({ reason: { code: "nope" } }))).toBe(
false
);
});
it("rejects missing action", () => {
const { action, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects action with string primary", () => {
expect(
isValidEnvelope(validEnvelope({ action: { primary: "setup" } }))
).toBe(false);
});
it("rejects action with array primary", () => {
expect(isValidEnvelope(validEnvelope({ action: { primary: [] } }))).toBe(
false
);
});
it("rejects action with incomplete primary (missing verb)", () => {
expect(
isValidEnvelope(validEnvelope({ action: { primary: { label: "x" } } }))
).toBe(false);
});
it("accepts action with null primary", () => {
expect(isValidEnvelope(validEnvelope({ action: { primary: null } }))).toBe(
true
);
});
it("accepts action with full setup primary", () => {
expect(
isValidEnvelope(validEnvelope({ action: { primary: FULL_SETUP_ACTION } }))
).toBe(true);
});
it("rejects missing updated_at", () => {
const { updated_at, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects missing ttl_seconds", () => {
const { ttl_seconds, ...rest } = validEnvelope();
expect(isValidEnvelope(rest)).toBe(false);
});
it("rejects non-number ttl_seconds", () => {
expect(isValidEnvelope(validEnvelope({ ttl_seconds: "3600" }))).toBe(false);
});
});
// ── 2. Stale / Invalid helpers (severity unknown, full probe action) ──
describe("isEnvelopeStale", () => {
function makeEnv(overrides: Partial<ProbeEnvelope> = {}): ProbeEnvelope {
return {
schema_version: 1,
module: "installation",
capability_state: "ready",
severity: "ok",
activity_state: "idle",
activity_label: null,
activity_progress: null,
notices: [],
reason: { code: "ok", text: "ok" },
action: { primary: null },
updated_at: new Date().toISOString(),
ttl_seconds: 3600,
...overrides,
};
}
it("returns true when ttl_seconds is 0", () => {
expect(isEnvelopeStale(makeEnv({ ttl_seconds: 0 }))).toBe(true);
});
it("returns true for epoch date", () => {
expect(
isEnvelopeStale(makeEnv({ updated_at: new Date(0).toISOString() }))
).toBe(true);
});
it("returns false when recent within TTL", () => {
expect(isEnvelopeStale(makeEnv({ ttl_seconds: 3600 }))).toBe(false);
});
});
describe("createStaleEnvelope", () => {
it("produces unknown state, module-prefixed reason, probe action", () => {
const env = createStaleEnvelope("help");
expect(env.capability_state).toBe("unknown");
expect(env.severity).toBe("unknown");
expect(env.reason.code).toBe("help.stale");
expect(env.ttl_seconds).toBe(0);
expect(env.activity_label).toBeNull();
expect(env.activity_progress).toBeNull();
expect(env.action.primary).not.toBeNull();
expect(env.action.primary!.verb).toBe("probe");
});
});
describe("createInvalidEnvelope", () => {
it("produces unknown state, module-prefixed reason, probe action", () => {
const env = createInvalidEnvelope("library");
expect(env.capability_state).toBe("unknown");
expect(env.severity).toBe("unknown");
expect(env.reason.code).toBe("library.invalid_response");
expect(env.activity_label).toBeNull();
expect(env.activity_progress).toBeNull();
expect(env.action.primary!.verb).toBe("probe");
});
});
// ── 3. createUnknownEnvelope ──
describe("createUnknownEnvelope", () => {
it("produces six distinct unknown envelopes", () => {
for (const mod of CAPABILITY_MODULES) {
const env = createUnknownEnvelope(mod);
expect(env.module).toBe(mod);
expect(env.capability_state).toBe("unknown");
expect(env.schema_version).toBe(SCHEMA_VERSION);
expect(env.activity_label).toBeNull();
expect(env.activity_progress).toBeNull();
}
});
it("ALL constituents get verb=probe, maintenance gets null", () => {
for (const mod of CAPABILITY_MODULES) {
const env = createUnknownEnvelope(mod);
if (mod === "maintenance") {
expect(env.action.primary).toBeNull();
} else {
expect(env.action.primary?.verb).toBe("probe");
}
}
});
});
// ── 4. Six-module grid & order ──
describe("CAPABILITY_MODULES", () => {
it("contains exactly six modules", () => {
expect(CAPABILITY_MODULES).toHaveLength(6);
});
it("renders in approved prototype order: installation, library, ocr, memory, maintenance, help", () => {
expect(CAPABILITY_MODULES[0]).toBe("installation");
expect(CAPABILITY_MODULES[1]).toBe("library");
expect(CAPABILITY_MODULES[2]).toBe("ocr");
expect(CAPABILITY_MODULES[3]).toBe("memory");
expect(CAPABILITY_MODULES[4]).toBe("maintenance");
expect(CAPABILITY_MODULES[5]).toBe("help");
});
it("includes all expected modules", () => {
expect(CAPABILITY_MODULES).toContain("installation");
expect(CAPABILITY_MODULES).toContain("library");
expect(CAPABILITY_MODULES).toContain("ocr");
expect(CAPABILITY_MODULES).toContain("memory");
expect(CAPABILITY_MODULES).toContain("maintenance");
expect(CAPABILITY_MODULES).toContain("help");
});
});
// ── 5. probeAction / setupAction builders ──
describe("probeAction", () => {
it("returns full ActionPrimary with correct verb", () => {
const a = probeAction("installation");
expect(a.verb).toBe("probe");
expect(a.label).toBe("Check");
expect(a.destructive).toBe(false);
expect(a.command).toBe("probe installation");
expect(a.scope).toBe("installation");
expect(a.scope_count).toBe(1);
});
});
describe("setupAction", () => {
it("returns full ActionPrimary with setup verb", () => {
const a = setupAction();
expect(a.verb).toBe("setup");
expect(a.label).toBe("Open Setup Wizard");
expect(a.destructive).toBe(false);
});
});
// ── 5. Ready envelope ──
describe("isReadyEnvelope", () => {
function make(overrides: Partial<ProbeEnvelope> = {}): ProbeEnvelope {
return {
schema_version: 1,
module: "installation",
capability_state: "ready",
severity: "ok",
activity_state: "idle",
activity_label: null,
activity_progress: null,
reason: { code: "ok", text: "ok" },
action: { primary: null },
notices: [],
updated_at: new Date().toISOString(),
ttl_seconds: 3600,
...overrides,
};
}
it("returns true for ready with null primary", () => {
expect(isReadyEnvelope(make())).toBe(true);
});
it("returns false for ready with non-null action", () => {
expect(
isReadyEnvelope(make({ action: { primary: FULL_SETUP_ACTION } }))
).toBe(false);
});
it("returns false for non-ready state", () => {
expect(isReadyEnvelope(make({ capability_state: "unavailable" }))).toBe(
false
);
});
it("returns false for unknown state", () => {
expect(isReadyEnvelope(createUnknownEnvelope("installation"))).toBe(false);
});
});
// ── 6. Six-module grid ──
describe("CAPABILITY_MODULES", () => {
it("contains exactly six modules", () => {
expect(CAPABILITY_MODULES).toHaveLength(6);
});
it("includes all expected modules", () => {
expect(CAPABILITY_MODULES).toContain("installation");
expect(CAPABILITY_MODULES).toContain("help");
expect(CAPABILITY_MODULES).toContain("library");
expect(CAPABILITY_MODULES).toContain("ocr");
expect(CAPABILITY_MODULES).toContain("memory");
expect(CAPABILITY_MODULES).toContain("maintenance");
});
});
// ── 7. Literal backend envelope test ──
describe("literal backend envelope", () => {
/** Exact shape a real `paperforge probe installation --json` would emit. */
const BACKEND_INSTALLATION_READY: Record<string, unknown> = {
schema_version: 1,
module: "installation",
capability_state: "ready",
activity_state: "idle",
activity_label: null,
activity_progress: null,
severity: "ok",
reason: { code: "ok", text: "PaperForge environment is set up correctly." },
action: { primary: null },
notices: [
{ level: "info", message: "Installation verified at 2026-01-15" },
],
updated_at: "2026-01-15T00:00:00.000Z",
ttl_seconds: 3600,
};
it("validates a ready backend envelope", () => {
expect(isValidEnvelope(BACKEND_INSTALLATION_READY)).toBe(true);
});
it("validates a backend envelope with full setup action", () => {
const envelope: Record<string, unknown> = {
...BACKEND_INSTALLATION_READY,
capability_state: "needs_action",
severity: "warning",
reason: {
code: "setup_required",
text: "Initial setup not yet complete.",
},
action: { primary: FULL_SETUP_ACTION },
};
expect(isValidEnvelope(envelope)).toBe(true);
});
it("validates a running backend envelope", () => {
const envelope: Record<string, unknown> = {
...BACKEND_INSTALLATION_READY,
capability_state: "unknown",
activity_state: "running",
activity_label: "Probing PaperForge installation...",
activity_progress: { current: 2, total: 5 },
severity: "unknown",
};
expect(isValidEnvelope(envelope)).toBe(true);
});
});
// ── 8. classifyCapabilityAction ──
describe("classifyCapabilityAction", () => {
const base: Partial<ProbeEnvelope> = {
schema_version: 1,
module: "installation",
capability_state: "needs_action",
activity_state: "idle",
severity: "warning",
reason: { code: "test", text: "test" },
updated_at: new Date().toISOString(),
ttl_seconds: 3600,
};
it("preserves the backend-selected action label", () => {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
module: "help",
action: {
primary: { verb: "setup", label: "Restore help", destructive: false },
},
};
expect(classifyCapabilityAction(env).label).toBe("Restore help");
});
it("classifies set_config verb as setup kind", () => {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
action: {
primary: { verb: "set_config", label: "Configure", destructive: false },
},
};
const result = classifyCapabilityAction(env);
expect(result.kind).toBe("setup");
expect(result.verb).toBe("set_config");
});
it("classifies update verb as setup kind", () => {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
action: {
primary: { verb: "update", label: "Update", destructive: false },
},
};
const result = classifyCapabilityAction(env);
expect(result.kind).toBe("setup");
expect(result.verb).toBe("update");
});
it("classifies probe verb as probe kind", () => {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
action: {
primary: { verb: "probe", label: "Refresh", destructive: false },
},
};
const result = classifyCapabilityAction(env);
expect(result.kind).toBe("probe");
});
it("classifies sync/run/rebuild_index as action kind", () => {
for (const verb of ["sync", "run", "rebuild_index", "migrate"]) {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
action: { primary: { verb, label: verb, destructive: false } },
};
expect(classifyCapabilityAction(env).kind).toBe("action");
}
});
it("defaults to probe when action is null", () => {
const env: ProbeEnvelope = { ...(base as ProbeEnvelope), action: null };
expect(classifyCapabilityAction(env).kind).toBe("probe");
});
it("defaults to probe when primary action is null", () => {
const env: ProbeEnvelope = {
...(base as ProbeEnvelope),
action: { primary: null },
};
expect(classifyCapabilityAction(env).kind).toBe("probe");
});
});
// ── 9. computeModuleSummary ──
describe("computeModuleSummary", () => {
const readyEnv: ProbeEnvelope = {
schema_version: 1,
module: "installation",
capability_state: "ready",
activity_state: "idle",
severity: "ok",
reason: null,
action: { primary: null },
updated_at: new Date().toISOString(),
ttl_seconds: 3600,
};
const unknownEnv: ProbeEnvelope = {
schema_version: 1,
module: "installation",
capability_state: "unknown",
activity_state: "idle",
severity: "unknown",
reason: { code: "test", text: "test" },
action: { primary: { verb: "probe", label: "Probe", destructive: false } },
updated_at: new Date(0).toISOString(),
ttl_seconds: 0,
};
const realModules = ["installation", "help"];
it("reports coreReady=true when all real modules are ready", () => {
const map: Record<string, ProbeEnvelope> = {
installation: { ...readyEnv, module: "installation" },
help: { ...readyEnv, module: "help" },
};
expect(computeModuleSummary(map, realModules).coreReady).toBe(true);
});
it("reports coreReady=false when a real module is unknown", () => {
const map: Record<string, ProbeEnvelope> = {
installation: unknownEnv,
help: { ...readyEnv, module: "help" },
};
const result = computeModuleSummary(map, realModules);
expect(result.coreReady).toBe(false);
expect(result.attentionModules).toContain("installation");
});
it("includes missing module as attention", () => {
const map: Record<string, ProbeEnvelope> = {
installation: { ...readyEnv, module: "installation" },
// help is missing
};
const result = computeModuleSummary(map, realModules);
expect(result.coreReady).toBe(false);
expect(result.attentionModules).toContain("help");
});
});
describe("validatePersistedEnvelopes", () => {
const allModules = ["installation", "help"];
const fresh = Date.now();
/** Build a full valid envelope matching worktree isValidEnvelope strict checks. */
function validEnv(mod: string): Record<string, unknown> {
return {
schema_version: 1,
module: mod,
capability_state: "ready",
activity_state: "idle",
activity_label: null,
activity_progress: null,
severity: "ok",
reason: { code: `${mod}.ready`, text: `${mod} is fully functional.` },
action: { primary: null },
notices: [],
updated_at: new Date(fresh).toISOString(),
ttl_seconds: 86400,
};
}
it("passes through valid fresh envelopes", () => {
const input: Record<string, unknown> = {
installation: validEnv("installation"),
help: validEnv("help"),
};
const result = validatePersistedEnvelopes(input, allModules);
expect(result["installation"].capability_state).toBe("ready");
expect(result["help"].capability_state).toBe("ready");
});
it("replaces malformed entries with invalid envelopes", () => {
const input: Record<string, unknown> = {
installation: { schema_version: "not-a-number", module: "installation" },
};
const result = validatePersistedEnvelopes(input, allModules);
expect(result["installation"].capability_state).toBe("unknown");
expect(result["installation"].reason?.code).toBe(
"installation.invalid_response"
);
});
it("replaces stale entries with stale envelopes", () => {
const oldDate = new Date(0).toISOString();
const input: Record<string, unknown> = {
installation: {
...validEnv("installation"),
updated_at: oldDate,
ttl_seconds: 1,
},
};
const result = validatePersistedEnvelopes(input, allModules);
expect(result["installation"].capability_state).toBe("unknown");
expect(result["installation"].reason?.code).toBe("installation.stale");
});
it("creates unknown envelopes for missing modules", () => {
const result = validatePersistedEnvelopes({}, allModules);
expect(result["installation"].capability_state).toBe("unknown");
expect(result["help"].capability_state).toBe("unknown");
});
it("replaces non-object entries with unknown envelopes", () => {
const input: Record<string, unknown> = { installation: "string-value" };
const result = validatePersistedEnvelopes(input, allModules);
expect(result["installation"].capability_state).toBe("unknown");
expect(result["installation"].reason?.code).toBe("installation.no_probe");
});
it("replaces entries with wrong module name with invalid envelope", () => {
const input: Record<string, unknown> = {
installation: { ...validEnv("help") }, // expects "installation" key but module="help"
};
const result = validatePersistedEnvelopes(input, allModules);
expect(result["installation"].capability_state).toBe("unknown");
expect(result["installation"].reason?.code).toBe(
"installation.invalid_response"
);
});
it("replaces entries with non-ready severity/state mismatch with invalid envelope", () => {
// isValidEnvelope checks validity not correctness — this passes validation
const input: Record<string, unknown> = {
installation: {
...validEnv("installation"),
capability_state: "unknown",
severity: "unknown",
updated_at: new Date(0).toISOString(),
ttl_seconds: 0,
},
};
const result = validatePersistedEnvelopes(input, allModules);
// Should be replaced as stale (TTL zero => always stale per isEnvelopeStale)
expect(result["installation"].capability_state).toBe("unknown");
expect(result["installation"].reason?.code).toBe("installation.stale");
});
});
// Register obsidian-test-mocks globals (createDiv, etc.) before any tests
import "obsidian-test-mocks/jest-setup";
// ── 10. Production-seam runtime dispatch (Issue #77) ──
import { PaperForgeSettingTab } from "../src/settings";
import { resolveRuntimeCommand } from "../src/services/managed-runtime";
import { getCachedPython } from "../src/services/memory-state";
const { mockExecFile, mockSpawn } = vi.hoisted(() => ({
mockExecFile: vi.fn(),
mockSpawn: vi.fn(() => ({
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn(),
})),
}));
const { mockResolveRuntimeCommand, mockGetCachedPython } = vi.hoisted(() => ({
mockResolveRuntimeCommand: vi.fn(),
mockGetCachedPython: vi.fn(),
}));
vi.mock("child_process", () => ({
default: {
execFile: mockExecFile,
execFileSync: () => "Python 3.11.0",
spawn: mockSpawn,
exec: vi.fn(),
},
execFile: mockExecFile,
execFileSync: () => "Python 3.11.0",
spawn: mockSpawn,
exec: vi.fn(),
}));
vi.mock("../src/services/managed-runtime", async (importOriginal) => ({
...(await importOriginal()),
resolveRuntimeCommand: mockResolveRuntimeCommand,
}));
vi.mock("../src/services/memory-state", async (importOriginal) => ({
...(await importOriginal()),
getCachedPython: mockGetCachedPython,
}));
describe("production-seam runtime dispatch", () => {
let app: Record<string, unknown>;
let plugin: Record<string, unknown>;
let tab: PaperForgeSettingTab;
let warnMessages: string[];
beforeAll(() => {
// Polyfill Obsidian HTMLElement prototype methods that JSDOM lacks
const win = globalThis.document?.defaultView;
const proto = win?.HTMLElement?.prototype;
if (!proto) return;
const polyfill = <T>(key: string, fn: T) => {
if (!(key in proto)) proto[key] = fn;
};
polyfill("empty", function (this: HTMLElement) {
this.innerHTML = "";
});
polyfill("appendText", function (this: HTMLElement, text: string) {
this.appendChild(this.ownerDocument.createTextNode(text));
});
polyfill(
"createDiv",
function (
this: HTMLElement,
opts?: Record<string, unknown>
): HTMLElement {
const el = document.createElement("div");
if (opts?.cls) el.className = String(opts.cls);
if (opts?.text) el.textContent = String(opts.text);
if (opts?.attr) {
const attrs = opts.attr as Record<string, string>;
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
}
this.appendChild(el);
return el;
}
);
polyfill(
"createEl",
function (
this: HTMLElement,
tag: string,
opts?: Record<string, unknown>
): HTMLElement {
const el = document.createElement(tag);
if (opts?.cls) el.className = String(opts.cls);
if (opts?.text) el.textContent = String(opts.text);
if (opts?.attr) {
const attrs = opts.attr as Record<string, string>;
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
}
this.appendChild(el);
return el;
}
);
polyfill(
"setAttr",
function (this: HTMLElement, attr: string, value: string): HTMLElement {
this.setAttribute(attr, value);
return this;
}
);
polyfill("setText", function (this: HTMLElement, text: string): void {
this.textContent = text;
});
});
function setManagedHealth(health: Record<string, unknown>): void {
(tab as any)._managedRuntime = {
current: () => health,
};
}
beforeEach(() => {
warnMessages = [];
vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
warnMessages.push(args.join(" "));
});
mockExecFile.mockClear();
mockSpawn.mockClear();
mockResolveRuntimeCommand.mockClear();
mockGetCachedPython.mockClear();
app = { vault: { adapter: { basePath: "/test/vault" } } };
plugin = {
settings: { capabilityState: {} },
manifest: { version: "2.1.0" },
saveSettings: vi.fn(),
readPaperforgeJson: () => ({}),
savePaperforgeJson: vi.fn(),
loadSettings: vi.fn(),
};
tab = new PaperForgeSettingTab(app as any, plugin as any);
// Pre-seed tab._capabilityState to prevent display() from auto-probing
// all modules (which causes recursive execFile calls)
tab._capabilityState = {};
for (const mod of CAPABILITY_MODULES) {
tab._capabilityState[mod] = {
schema_version: 1,
module: mod,
capability_state: "ready",
activity_state: "idle",
activity_label: null,
activity_progress: null,
severity: "ok",
reason: { code: `${mod}.ready`, text: `${mod} is ready` },
action: { primary: null },
notices: [],
updated_at: new Date().toISOString(),
ttl_seconds: 3600,
};
}
(plugin.settings as Record<string, unknown>).capabilityState = {
...tab._capabilityState,
};
});
it("uses managed python path in _probeModule when runtime is ready", () => {
setManagedHealth({
state: "ready",
pythonPath: "/managed/python",
version: "1.0.0",
source: "venv",
error: null,
lastVerifiedAt: "2026-01-01T00:00:00.000Z",
stale: false,
});
mockResolveRuntimeCommand.mockReturnValue({
command: "/managed/python",
args: [],
});
mockGetCachedPython.mockReturnValue({
path: "",
source: "manual" as const,
extraArgs: [],
});
tab._probeModule("installation");
expect(mockExecFile).toHaveBeenCalledTimes(1);
const execPath: string = mockExecFile.mock.calls[0][0];
expect(execPath).toBe("/managed/python");
const execArgs: string[] = mockExecFile.mock.calls[0][1];
expect(execArgs).not.toContain("-3");
expect(execArgs).toContain("-m");
expect(execArgs).toContain("paperforge");
expect(execArgs).toContain("probe");
expect(execArgs).toContain("installation");
expect(execArgs).toContain("--json");
expect(mockExecFile.mock.calls[0][3]).toBeInstanceOf(Function);
});
it("uses managed python path in _callPython when runtime is ready", () => {
setManagedHealth({
state: "ready",
pythonPath: "/managed/python",
version: "1.0.0",
source: "venv",
error: null,
lastVerifiedAt: "2026-01-01T00:00:00.000Z",
stale: false,
});
mockResolveRuntimeCommand.mockReturnValue({
command: "/managed/python",
args: [],
});
mockGetCachedPython.mockReturnValue({
path: "",
source: "manual" as const,
extraArgs: [],
});
tab._callPython(["sync"], {
timeout: 5000,
onClose: vi.fn(),
});
expect(mockExecFile).toHaveBeenCalledTimes(1);
const execPath: string = mockExecFile.mock.calls[0][0];
expect(execPath).toBe("/managed/python");
const execArgs: string[] = mockExecFile.mock.calls[0][1];
expect(execArgs).not.toContain("-3");
expect(execArgs).toContain("-m");
expect(execArgs).toContain("paperforge");
expect(execArgs).toContain("sync");
const execOpts: Record<string, unknown> = mockExecFile.mock.calls[0][2];
expect(execOpts.timeout).toBe(5000);
expect(mockExecFile.mock.calls[0][3]).toBeInstanceOf(Function);
});
it("fails closed when managed runtime is cold/stale — no legacy fallback", () => {
setManagedHealth({
state: "unknown",
pythonPath: null,
version: null,
source: "none",
error: null,
lastVerifiedAt: null,
stale: true,
});
mockResolveRuntimeCommand.mockReturnValue(null);
mockGetCachedPython.mockReturnValue({
path: "/legacy/python",
source: "auto-detected" as const,
extraArgs: ["-3"],
});
tab._probeModule("installation");
// Legacy fallback is removed — no execFile, no warning, setup envelope
expect(mockExecFile).not.toHaveBeenCalled();
const env = tab._capabilityState?.["installation"];
expect(env?.reason?.code).toBe("installation.no_python");
expect(env?.action?.primary?.verb).toBe("setup");
});
it("RED Gap 2 follow-up: null resolver produces persistent setup envelope (installation.no_python + setup verb)", () => {
(tab as any)._managedRuntime = null;
mockResolveRuntimeCommand.mockReturnValue(null);
mockGetCachedPython.mockReturnValue({
path: "",
source: "manual" as const,
extraArgs: [],
});
tab._probeModule("installation");
expect(mockExecFile).not.toHaveBeenCalled();
const env = tab._capabilityState?.["installation"];
expect(env?.reason?.code).toBe("installation.no_python");
expect(env?.action?.primary?.verb).toBe("setup");
});
it("propagates execFile timeout and stream behavior through managed path", () => {
setManagedHealth({
state: "ready",
pythonPath: "/managed/python",
version: "1.0.0",
source: "venv",
error: null,
lastVerifiedAt: "2026-01-01T00:00:00.000Z",
stale: false,
});
mockResolveRuntimeCommand.mockReturnValue({
command: "/managed/python",
args: [],
});
mockGetCachedPython.mockReturnValue({
path: "",
source: "manual" as const,
extraArgs: [],
});
// Stream path — spawn called
tab._callPython(["embed", "build"], {
stream: true,
onData: vi.fn(),
onClose: vi.fn(),
});
expect(mockSpawn).toHaveBeenCalledTimes(1);
const spawnPath: string = mockSpawn.mock.calls[0][0];
expect(spawnPath).toBe("/managed/python");
const spawnArgs: string[] = mockSpawn.mock.calls[0][1];
expect(spawnArgs).not.toContain("-3");
expect(spawnArgs).toContain("embed");
expect(spawnArgs).toContain("build");
// Non-stream path with timeout
tab._callPython(["retrieval", "status", "--json"], {
timeout: 8000,
onClose: vi.fn(),
});
expect(mockExecFile).toHaveBeenCalledTimes(1);
const execPath: string = mockExecFile.mock.calls[0][0];
expect(execPath).toBe("/managed/python");
const execOpts: Record<string, unknown> = mockExecFile.mock.calls[0][2];
expect(execOpts.timeout).toBe(8000);
});
it("managed absent and legacy empty path produces stale envelope without fallback warning", () => {
(tab as any)._managedRuntime = null;
mockResolveRuntimeCommand.mockReturnValue(null);
mockGetCachedPython.mockReturnValue({
path: "",
source: "manual" as const,
extraArgs: [],
});
tab._probeModule("help");
expect(mockExecFile).not.toHaveBeenCalled();
// No Release N legacy fallback warning — fail closed
expect(warnMessages.some((w) => w.includes("Release N"))).toBe(false);
expect(tab._capabilityState?.["help"]?.reason?.code).toBe(
"help.invalid_response"
);
});
});