mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
- codemode:Tool Search 常驻(alwaysEnabled)、权限 allow(嵌套调用仍逐个过权限门)、 tools.jsonc 登记使用规则;RTK 跳过 parentToolCallId 嵌套调用,避免丢失 bash structuredContent;Tool Search 目录过滤 hidden / codemode(script-only)exposure; 扩展加载测试加载 builtin:codemode 并断言其常驻激活 - 搜索 key 不再经命令行参数传递:search-auth.mjs 改为 --set-stdin(先读校验再加锁), configure.sh 交互输入经管道、非交互用 --<provider> - - CodeGraph 客户端监听子进程 stdin error,避免 EPIPE 未处理导致崩溃 - 移除 PI_TOOL_SEARCH/HERMES_MEMORY/RTK_CONFIG_PATH 环境变量入口,测试改用工厂 configPath 注入 - pi-subagents 固定 vitest / coverage-istanbul 4.1.10,修复 npm ls 版本冲突 - 补 pi-web-search / pi-worktree UPSTREAM.md 与 pi-minimal-footer AGENTS.md, files 白名单补齐并加测试;.codegraph/.gitignore 移出跟踪 - detect_kitty 优先识别官方安装目录 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
108 lines
4.7 KiB
TypeScript
108 lines
4.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join, posix, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
|
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const packageJson = JSON.parse(readFileSync(join(repositoryRoot, "package.json"), "utf8")) as {
|
|
dependencies: Record<string, string>;
|
|
pi: { extensions: string[] };
|
|
};
|
|
|
|
const packOutput = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
|
|
cwd: repositoryRoot,
|
|
encoding: "utf8",
|
|
});
|
|
const packResult = JSON.parse(packOutput.slice(packOutput.indexOf("["))) as Array<{
|
|
files: Array<{ path: string }>;
|
|
}>;
|
|
const packedFiles = new Set(packResult[0].files.map((entry) => entry.path));
|
|
|
|
test("the packed bundle ships no nested node_modules or build output", () => {
|
|
assert.ok(packedFiles.size > 0, "npm pack --dry-run must report a non-empty file list");
|
|
for (const filePath of packedFiles) {
|
|
const segments = filePath.split("/");
|
|
assert.equal(segments.includes("node_modules"), false, `${filePath} must not ship a nested node_modules tree`);
|
|
assert.equal(segments.includes("dist"), false, `${filePath} must not ship build output`);
|
|
}
|
|
});
|
|
|
|
test("every declared extension entry and file: dependency manifest is inside the packed bundle", () => {
|
|
for (const entry of packageJson.pi.extensions) {
|
|
const bundled = entry.replace(/^\.\//u, "");
|
|
assert.ok(packedFiles.has(bundled), `extension entry ${entry} must be inside the packed bundle`);
|
|
}
|
|
for (const [name, specifier] of Object.entries(packageJson.dependencies)) {
|
|
const match = /^file:\.\/(.+)$/u.exec(specifier);
|
|
if (!match) continue;
|
|
const manifest = `${match[1]}/package.json`;
|
|
assert.ok(packedFiles.has(manifest), `${name} is a file: dependency — ${manifest} must be packed`);
|
|
}
|
|
});
|
|
|
|
test("every file: dependency ships an UPSTREAM.md source record", () => {
|
|
for (const [name, specifier] of Object.entries(packageJson.dependencies)) {
|
|
const match = /^file:\.\/(.+)$/u.exec(specifier);
|
|
if (!match) continue;
|
|
const upstream = `${match[1]}/UPSTREAM.md`;
|
|
assert.ok(
|
|
readFileSync(join(repositoryRoot, upstream), "utf8").trim().length > 0,
|
|
`${name} is a file: dependency — ${upstream} must exist and be non-empty`,
|
|
);
|
|
assert.ok(packedFiles.has(upstream), `${name} is a file: dependency — ${upstream} must be packed`);
|
|
}
|
|
});
|
|
|
|
test("relative import specifiers inside packed sources resolve to packed files", () => {
|
|
const specifierPatterns = [
|
|
/\bfrom\s+["'](\.{1,2}\/[^"']+)["']/gu,
|
|
/(?<![\w$.])import\s*\(\s*["'](\.{1,2}\/[^"']+)["']\s*\)/gu,
|
|
/\bnew\s+URL\s*\(\s*["'](\.{1,2}\/[^"']+)["']\s*,\s*import\.meta\.url\s*\)/gu,
|
|
];
|
|
const isPacked = (resolved: string): boolean => {
|
|
if (packedFiles.has(resolved)) return true;
|
|
const candidates = [`${resolved}.ts`, `${resolved}.js`, `${resolved}.mjs`, `${resolved}/index.ts`];
|
|
if (candidates.some((candidate) => packedFiles.has(candidate))) return true;
|
|
// TypeScript sources import sibling ".js"/".mjs" specifiers that map to ".ts" files.
|
|
const swapped = resolved.replace(/\.(js|mjs|cjs)$/u, ".ts");
|
|
if (swapped !== resolved && packedFiles.has(swapped)) return true;
|
|
// new URL("../config/prompt/", import.meta.url) style directory references.
|
|
for (const filePath of packedFiles) {
|
|
if (filePath.startsWith(`${resolved}/`)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
const unresolved: string[] = [];
|
|
for (const filePath of packedFiles) {
|
|
if (!/\.(ts|js|mjs)$/u.test(filePath)) continue;
|
|
const segments = filePath.split("/");
|
|
if (/\.test\.(ts|js|mjs)$/u.test(filePath) || segments.includes("test") || segments.includes("tests")) continue;
|
|
const source = readFileSync(join(repositoryRoot, filePath), "utf8");
|
|
for (const pattern of specifierPatterns) {
|
|
pattern.lastIndex = 0;
|
|
for (const match of source.matchAll(pattern)) {
|
|
const resolved = posix.normalize(posix.join(posix.dirname(filePath), match[1])).replace(/\/$/u, "");
|
|
if (resolved === ".." || resolved.startsWith("../")) continue;
|
|
if (resolved.split("/").includes("node_modules")) continue;
|
|
if (!isPacked(resolved)) unresolved.push(`${filePath} -> ${match[1]}`);
|
|
}
|
|
}
|
|
}
|
|
assert.deepEqual(unresolved, [], "packed sources reference files missing from the bundle");
|
|
});
|
|
|
|
test("prompt config and lifecycle scripts are inside the packed bundle", () => {
|
|
const tracked = execFileSync("git", ["ls-files", "config/prompt"], {
|
|
cwd: repositoryRoot,
|
|
encoding: "utf8",
|
|
})
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
for (const filePath of [...tracked, "install.sh", "update.sh", "uninstall.sh", "configure.sh", "scripts/lib.sh"]) {
|
|
assert.ok(packedFiles.has(filePath), `${filePath} must be inside the packed bundle`);
|
|
}
|
|
});
|