mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 18:10:39 +09:00
## Thinking Path > - Paperclip is a local-first control plane for AI-agent companies. > - Operators need predictable local dev behavior, recoverable instance data, and scripts that do not churn the running app. > - Several accumulated changes improve backup streaming, dev-server health, static UI caching/logging, diagnostic-file ignores, and instance isolation. > - These are operational improvements that can land independently from product UI work. > - This pull request groups the dev-infra and backup changes from the split branch into one standalone branch. > - The benefit is safer local operation, easier manual backups, less noisy dev output, and less cross-instance auth leakage. ## What Changed - Added a manual instance database backup endpoint and route tests. - Streamed backup/restore handling to avoid materializing large payloads at once. - Reduced dev static UI log/cache churn and ignored Node diagnostic report captures. - Added guarded dev auto-restart health polling coverage. - Preserved worktree config during provisioning and scoped auth cookies by instance. - Added a Discord daily digest helper script and environment documentation. - Hardened adapter-route and startup feedback export tests around the changed infrastructure. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run packages/db/src/backup-lib.test.ts server/src/__tests__/instance-database-backups-routes.test.ts server/src/__tests__/server-startup-feedback-export.test.ts server/src/__tests__/adapter-routes.test.ts server/src/__tests__/dev-runner-paths.test.ts server/src/__tests__/health-dev-server-token.test.ts server/src/__tests__/http-log-policy.test.ts server/src/__tests__/vite-html-renderer.test.ts server/src/__tests__/workspace-runtime.test.ts server/src/__tests__/better-auth.test.ts` - Split integration check: merged after the runtime/governance branch and before UI branches with no merge conflicts. - Confirmed this branch does not include `pnpm-lock.yaml`. ## Risks - Medium risk: touches server startup, backup streaming, auth cookie naming, dev health checks, and worktree provisioning. - Backup endpoint behavior depends on existing board/admin access controls and database backup helpers. - No database migrations are included. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic code-editing/runtime with local shell and GitHub CLI access; exact context window and reasoning mode are not exposed by the Paperclip harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createCachedViteHtmlRenderer, type ViteWatcherHost } from "../vite-html-renderer.js";
|
|
|
|
function createWatcher() {
|
|
const listeners = new Map<string, Set<(file: string) => void>>();
|
|
|
|
return {
|
|
on(event: string, listener: (file: string) => void) {
|
|
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
listeners.get(event)?.add(listener);
|
|
},
|
|
off(event: string, listener: (file: string) => void) {
|
|
listeners.get(event)?.delete(listener);
|
|
},
|
|
emit(event: string, file: string) {
|
|
for (const listener of listeners.get(event) ?? []) {
|
|
listener(file);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("createCachedViteHtmlRenderer", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("reuses the injected dev html shell until index.html changes", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-vite-html-"));
|
|
tempDirs.push(tempDir);
|
|
const indexPath = path.join(tempDir, "index.html");
|
|
fs.writeFileSync(
|
|
indexPath,
|
|
'<html><body>v1<script type="module" src="/src/main.tsx"></script></body></html>',
|
|
"utf8",
|
|
);
|
|
|
|
const watcher = createWatcher();
|
|
const vite: ViteWatcherHost = {
|
|
watcher,
|
|
};
|
|
|
|
const renderer = createCachedViteHtmlRenderer({ vite, uiRoot: tempDir });
|
|
|
|
await expect(renderer.render("/")).resolves.toContain("/@vite/client");
|
|
await expect(renderer.render("/")).resolves.toContain('"/@react-refresh"');
|
|
const first = await renderer.render("/");
|
|
const second = await renderer.render("/issues");
|
|
expect(first).toBe(second);
|
|
expect(first.match(/\/@vite\/client/g)?.length).toBe(1);
|
|
expect(first).toContain("window.$RefreshReg$");
|
|
|
|
const sourcePath = path.join(tempDir, "src", "main.tsx");
|
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
fs.writeFileSync(sourcePath, "export {};\n", "utf8");
|
|
watcher.emit("change", sourcePath);
|
|
expect(await renderer.render("/")).toBe(first);
|
|
|
|
fs.writeFileSync(
|
|
indexPath,
|
|
'<html><body>v2<script type="module" src="/src/main.tsx"></script></body></html>',
|
|
"utf8",
|
|
);
|
|
watcher.emit("change", indexPath);
|
|
|
|
await expect(renderer.render("/")).resolves.toContain("v2");
|
|
|
|
renderer.dispose();
|
|
});
|
|
|
|
it("does not duplicate the vite client tag or react refresh preamble when already present", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-vite-html-"));
|
|
tempDirs.push(tempDir);
|
|
fs.writeFileSync(
|
|
path.join(tempDir, "index.html"),
|
|
'<html><head><script type="module">import { injectIntoGlobalHook } from "/@react-refresh";injectIntoGlobalHook(window);window.$RefreshReg$ = () => {};window.$RefreshSig$ = () => (type) => type;</script></head><body><script type="module" src="/@vite/client"></script><script type="module" src="/src/main.tsx"></script></body></html>',
|
|
"utf8",
|
|
);
|
|
|
|
const vite: ViteWatcherHost = {
|
|
watcher: createWatcher(),
|
|
};
|
|
|
|
const renderer = createCachedViteHtmlRenderer({ vite, uiRoot: tempDir });
|
|
|
|
const html = await renderer.render("/");
|
|
expect(html.match(/\/@vite\/client/g)?.length).toBe(1);
|
|
expect(html.match(/\/@react-refresh/g)?.length).toBe(1);
|
|
});
|
|
});
|