mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
fix: complete managed runtime cutover
This commit is contained in:
parent
9a15f36485
commit
b41b4c88b7
4 changed files with 1624 additions and 1468 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -2293,7 +2293,7 @@ export class PaperForgeSettingTab extends PluginSettingTab {
|
|||
0
|
||||
);
|
||||
try {
|
||||
const env = Object.assign({}, process.env, {
|
||||
const env = Object.assign(paperforgeEnrichedEnv(), {
|
||||
PYTHONIOENCODING: "utf-8",
|
||||
PYTHONUTF8: "1",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,10 +33,7 @@ import {
|
|||
paperforgeEnrichedEnv,
|
||||
buildTargetedEnv,
|
||||
} from "../services/python-bridge";
|
||||
import {
|
||||
ManagedRuntime,
|
||||
resolveRuntimeCommand,
|
||||
} from "../services/managed-runtime";
|
||||
import { resolveRuntimeCommand } from "../services/managed-runtime";
|
||||
import type { PluginForSecrets } from "../services/secret-storage";
|
||||
import { getDisclosureState, toggleDisclosureState } from "../utils/disclosure";
|
||||
import { extractZoteroKeyFromPath } from "../utils/zotero-path";
|
||||
|
|
@ -67,22 +64,13 @@ export class PaperForgeStatusView extends ItemView {
|
|||
_cachedItems: any[] | null = null;
|
||||
_modeSubscribers: { event: string; ref: any }[] = [];
|
||||
|
||||
_resolvePython(): { path: string; args: string[] } {
|
||||
_resolvePython(): { path: string; args: string[] } | null {
|
||||
const plugin = ((this.app as any).plugins.plugins as any)["paperforge"];
|
||||
const vp = (this.app.vault.adapter as any).basePath as string;
|
||||
const rt = new ManagedRuntime({
|
||||
runtimeDir: path.join(vp, ".paperforge-test-venv"),
|
||||
pluginVersion: plugin?.manifest?.version || "0.0.0",
|
||||
osPlatform: process.platform,
|
||||
osArch: process.arch,
|
||||
fs: fs as any,
|
||||
execFile: execFile as any,
|
||||
execFileSync: execFileSync as any,
|
||||
});
|
||||
const run = resolveRuntimeCommand(rt.current());
|
||||
return run
|
||||
? { path: run.command, args: [...run.args] }
|
||||
: { path: "python", args: [] };
|
||||
const mr = plugin?.getManagedRuntime?.();
|
||||
if (!mr) return null;
|
||||
const run = resolveRuntimeCommand(mr.current());
|
||||
if (!run) return null;
|
||||
return { path: run.command, args: [...run.args] };
|
||||
}
|
||||
_leafChangeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
_ocrPrivacyShown = false;
|
||||
|
|
@ -238,7 +226,9 @@ export class PaperForgeStatusView extends ItemView {
|
|||
"paperforge"
|
||||
] as any;
|
||||
const pluginVer = plugin?.manifest?.version || "?";
|
||||
const { path: pythonExe, args = [] } = this._resolvePython();
|
||||
const py = this._resolvePython();
|
||||
if (!py) return;
|
||||
const { path: pythonExe, args = [] } = py;
|
||||
Promise.resolve({ status: "ok", pyVersion: "?" }).then((result: any) => {
|
||||
if (result.status === "not-installed") return;
|
||||
const v = result.pyVersion || "";
|
||||
|
|
@ -277,7 +267,12 @@ export class PaperForgeStatusView extends ItemView {
|
|||
const plugin = ((this.app as any).plugins.plugins as any)[
|
||||
"paperforge"
|
||||
] as any;
|
||||
const { path: pythonExe, args = [] } = this._resolvePython();
|
||||
const py = this._resolvePython();
|
||||
if (!py) {
|
||||
this._fallbackFetchStats(quiet, vp, plugin);
|
||||
return;
|
||||
}
|
||||
const { path: pythonExe, args = [] } = py;
|
||||
(execFile as any)(
|
||||
pythonExe,
|
||||
[...args, "-m", "paperforge", "dashboard", "--json"],
|
||||
|
|
@ -416,7 +411,17 @@ export class PaperForgeStatusView extends ItemView {
|
|||
text: "No index \u2014 trying CLI...",
|
||||
});
|
||||
}
|
||||
const { path: pythonExe, args = [] } = this._resolvePython();
|
||||
const py = this._resolvePython();
|
||||
if (!py) {
|
||||
if (!this._cachedStats) {
|
||||
this._metricsEl!.createEl("div", {
|
||||
cls: "paperforge-status-error",
|
||||
text: "Cannot reach PaperForge CLI.\nMake sure paperforge is installed and in your PATH.",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { path: pythonExe, args = [] } = py;
|
||||
(execFile as any)(
|
||||
pythonExe,
|
||||
[...args, "-m", "paperforge", "status", "--json"],
|
||||
|
|
@ -1027,19 +1032,22 @@ export class PaperForgeStatusView extends ItemView {
|
|||
const pluginVer = plugin?.manifest?.version || "?";
|
||||
let pyVer = this._paperforgeVersion;
|
||||
if (!pyVer) {
|
||||
try {
|
||||
const vp = (this.app.vault.adapter as any).basePath as string;
|
||||
const { path: pyExe, args = [] } = this._resolvePython();
|
||||
const raw = execFileSync(
|
||||
pyExe,
|
||||
[...args, "-c", "import paperforge; print(paperforge.__version__)"],
|
||||
{ cwd: vp, timeout: 5000, encoding: "utf-8", windowsHide: true }
|
||||
).trim();
|
||||
if (raw) {
|
||||
pyVer = raw.startsWith("v") ? raw : "v" + raw;
|
||||
this._paperforgeVersion = pyVer;
|
||||
}
|
||||
} catch {}
|
||||
const py = this._resolvePython();
|
||||
if (py) {
|
||||
const { path: pyExe, args = [] } = py;
|
||||
try {
|
||||
const vp = (this.app.vault.adapter as any).basePath as string;
|
||||
const raw = execFileSync(
|
||||
pyExe,
|
||||
[...args, "-c", "import paperforge; print(paperforge.__version__)"],
|
||||
{ cwd: vp, timeout: 5000, encoding: "utf-8", windowsHide: true }
|
||||
).trim();
|
||||
if (raw) {
|
||||
pyVer = raw.startsWith("v") ? raw : "v" + raw;
|
||||
this._paperforgeVersion = pyVer;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
pyVer = pyVer || "\u2014";
|
||||
const runtimeOk = pyVer === "v" + pluginVer;
|
||||
|
|
@ -1085,27 +1093,7 @@ export class PaperForgeStatusView extends ItemView {
|
|||
);
|
||||
// Issue #79: check configured flag; getSecret is async so use boolean status
|
||||
const pfPlugin = (this.app as any).plugins?.plugins?.["paperforge"];
|
||||
let tokenOk = !!pfPlugin?.settings?._paddleocr_configured;
|
||||
if (!tokenOk) {
|
||||
try {
|
||||
const sysDir = plugin?.settings?.system_dir || "System";
|
||||
const envPath = path.join(vp, sysDir, "PaperForge", ".env");
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, "utf-8");
|
||||
const tokenMatch = envContent.match(
|
||||
/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m
|
||||
);
|
||||
tokenOk = !!(tokenMatch && tokenMatch[1] && tokenMatch[1].trim());
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!tokenOk) {
|
||||
tokenOk = !!(
|
||||
process.env.PADDLEOCR_API_TOKEN ||
|
||||
process.env.PADDLEOCR_API_KEY ||
|
||||
process.env.OCR_TOKEN
|
||||
);
|
||||
}
|
||||
const tokenOk = !!pfPlugin?.settings?._paddleocr_configured;
|
||||
this._renderSystemStatusRow(
|
||||
statusGrid,
|
||||
"OCR Token",
|
||||
|
|
@ -2642,13 +2630,14 @@ export class PaperForgeStatusView extends ItemView {
|
|||
const vp = (
|
||||
this.app.vault.adapter as unknown as Record<string, unknown>
|
||||
)["basePath"];
|
||||
if (typeof vp === "string") {
|
||||
const { path: pyExe, args = [] } = this._resolvePython();
|
||||
spawn(pyExe, [...args, "-m", "paperforge", "doctor"], {
|
||||
cwd: vp,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
if (typeof vp !== "string") return;
|
||||
const py = this._resolvePython();
|
||||
if (!py) return;
|
||||
const { path: pyExe, args = [] } = py;
|
||||
spawn(pyExe, [...args, "-m", "paperforge", "doctor"], {
|
||||
cwd: vp,
|
||||
stdio: "inherit",
|
||||
});
|
||||
});
|
||||
const retryBtn = actions.createEl("button", {
|
||||
cls: "pf-btn-secondary",
|
||||
|
|
@ -2794,7 +2783,13 @@ export class PaperForgeStatusView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
const { path: pythonExe, args: pyExtra = [] } = this._resolvePython();
|
||||
const py = this._resolvePython();
|
||||
if (!py) {
|
||||
this._searchState = "backend-unavailable";
|
||||
this._renderSearchState();
|
||||
return;
|
||||
}
|
||||
const { path: pythonExe, args: pyExtra = [] } = py;
|
||||
const deepFlag = mode === "retrieve" ? ["--deep"] : [];
|
||||
// Issue #79: resolve memory credentials immediately before search/retrieve spawn
|
||||
const searchEnv = await buildTargetedEnv(
|
||||
|
|
@ -3149,7 +3144,14 @@ export class PaperForgeStatusView extends ItemView {
|
|||
extraArgs = [...extraArgs, "--all"];
|
||||
}
|
||||
const cmdTimeout = a.needsFilter ? 60000 : a.needsKey ? 30000 : 600000;
|
||||
const { path: pythonExe, args: pyExtra = [] } = this._resolvePython();
|
||||
const py = this._resolvePython();
|
||||
if (!py) {
|
||||
this._showMessage("[!!] Runtime not available", "error");
|
||||
new Notice("[!!] PaperForge runtime is not ready. Check settings.", 6000);
|
||||
card.removeClass("running");
|
||||
return;
|
||||
}
|
||||
const { path: pythonExe, args: pyExtra = [] } = py;
|
||||
// Issue #79: resolve credentials for allowlisted command types immediately before launch
|
||||
const actionEnv = await buildTargetedEnv(
|
||||
{ app: this.app } as unknown as PluginForSecrets,
|
||||
|
|
|
|||
146
paperforge/plugin/tests/dashboard-runtime.test.ts
Normal file
146
paperforge/plugin/tests/dashboard-runtime.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Vitest tests for PaperForgeStatusView._resolvePython() — Issue #81.
|
||||
*
|
||||
* Verifies the method uses the plugin singleton's ManagedRuntime and never
|
||||
* constructs its own or falls back to ambient `python`.
|
||||
*/
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import "obsidian-test-mocks/jest-setup";
|
||||
|
||||
// Mock obsidian — minimal stubs needed to construct PaperForgeStatusView
|
||||
vi.mock("obsidian", () => {
|
||||
class MockComponent {
|
||||
register() {}
|
||||
registerEvent() {}
|
||||
registerDomEvent() {}
|
||||
registerInterval() {
|
||||
return 0;
|
||||
}
|
||||
load() {}
|
||||
unload() {}
|
||||
}
|
||||
class MockView extends MockComponent {
|
||||
app: any;
|
||||
contentEl: HTMLElement;
|
||||
constructor(leaf: any) {
|
||||
super();
|
||||
this.contentEl = document.createElement("div");
|
||||
}
|
||||
}
|
||||
return {
|
||||
ItemView: MockView,
|
||||
WorkspaceLeaf: class {},
|
||||
View: MockView,
|
||||
Component: MockComponent,
|
||||
Notice: class {
|
||||
noticeEl: HTMLElement;
|
||||
constructor(msg: string) {
|
||||
this.noticeEl = document.createElement("div");
|
||||
this.noticeEl.textContent = msg;
|
||||
}
|
||||
},
|
||||
Modal: class {
|
||||
contentEl: HTMLElement;
|
||||
constructor() {
|
||||
this.contentEl = document.createElement("div");
|
||||
}
|
||||
open() {}
|
||||
close() {}
|
||||
},
|
||||
MarkdownRenderer: { render: () => {} },
|
||||
App: class {},
|
||||
TFile: class {},
|
||||
};
|
||||
});
|
||||
|
||||
import { PaperForgeStatusView } from "../src/views/dashboard";
|
||||
|
||||
/** Mock health representing a ready runtime. */
|
||||
function readyHealth(pythonPath: string) {
|
||||
return { state: "ready" as const, pythonPath };
|
||||
}
|
||||
|
||||
/** Mock health for any non-ready state. */
|
||||
const notReadyHealth = { state: "needs_repair" as const, pythonPath: null };
|
||||
|
||||
describe("PaperForgeStatusView._resolvePython", () => {
|
||||
let leaf: any;
|
||||
let app: any;
|
||||
|
||||
function createView(): PaperForgeStatusView {
|
||||
return new (PaperForgeStatusView as any)(leaf);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
leaf = {};
|
||||
app = { plugins: { plugins: {} } };
|
||||
});
|
||||
|
||||
it("returns null when no paperforge plugin is registered", () => {
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
expect((view as any)._resolvePython()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the registered plugin has no getManagedRuntime", () => {
|
||||
app.plugins.plugins["paperforge"] = {};
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
expect((view as any)._resolvePython()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the managed runtime is not ready", () => {
|
||||
const mockRuntime = { current: () => notReadyHealth };
|
||||
app.plugins.plugins["paperforge"] = {
|
||||
getManagedRuntime: () => mockRuntime,
|
||||
};
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
expect((view as any)._resolvePython()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the singleton command when the managed runtime is ready", () => {
|
||||
const mockRuntime = {
|
||||
current: () => readyHealth("/opt/paperforge/venv/bin/python3"),
|
||||
};
|
||||
app.plugins.plugins["paperforge"] = {
|
||||
getManagedRuntime: () => mockRuntime,
|
||||
};
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
const result = (view as any)._resolvePython();
|
||||
expect(result).toEqual({
|
||||
path: "/opt/paperforge/venv/bin/python3",
|
||||
args: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT fall back to ambient 'python' when runtime is missing", () => {
|
||||
// If there is no plugin with getManagedRuntime, result must be null
|
||||
// rather than { path: "python", args: [] }
|
||||
app.plugins.plugins["paperforge"] = {};
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
const result = (view as any)._resolvePython();
|
||||
expect(result).toBeNull();
|
||||
// Specifically assert NOT { path: "python" }
|
||||
expect(result).not.toEqual(expect.objectContaining({ path: "python" }));
|
||||
});
|
||||
|
||||
it("passes args from the runtime command when present", () => {
|
||||
// resolveRuntimeCommand always returns args: [] currently,
|
||||
// but test the contract so it survives future changes
|
||||
const mockRuntime = {
|
||||
current: () => readyHealth("/usr/bin/python3.11"),
|
||||
};
|
||||
app.plugins.plugins["paperforge"] = {
|
||||
getManagedRuntime: () => mockRuntime,
|
||||
};
|
||||
const view = createView();
|
||||
(view as any).app = app;
|
||||
const result = (view as any)._resolvePython();
|
||||
expect(result).toHaveProperty("path", "/usr/bin/python3.11");
|
||||
expect(result).toHaveProperty("args");
|
||||
expect(Array.isArray(result!.args)).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue