Files
gitea/test/verify-real.mjs

242 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 真机验证:读 profile 真实配置 → 装载真实插件 → 对真实 Gitea 跑 提交/拉取/归档。
// 每工作区仓库模式:默认用临时合成工作区 dsh-verify-ws(隔离 mapFile),结尾删除测试仓库。
// 用法:node test/verify-real.mjs [--workspace <dir>](覆盖工作区目录,仓库名 = 目录 basename)
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const projectRoot = dirname(here);
const require_ = createRequire(import.meta.url);
const yaml = require_('/usr/local/nodejs/lib/node_modules/@deepseek-ai/dsh/node_modules/js-yaml');
// 1) 真实 profile 配置
const patch = yaml.load(require_('node:fs').readFileSync('/home/ai/.dsh/profiles/web/cordis.patch.yml', 'utf8'));
const row = patch.flatMap((entry) => Array.isArray(entry.insert) ? entry.insert : entry.id ? [entry] : [])
.find((entry) => entry?.id === 'gitea-publisher');
assert.ok(row?.config?.gitea, 'profile 里找不到 gitea-publisher 行');
const giteaConfig = structuredClone(row.config.gitea);
for (const key of ['baseUrl', 'token', 'repo']) assert.ok(giteaConfig[key], `profile 配置 ${key} 为空`);
const secret = giteaConfig.token;
giteaConfig.mapFile = join(mkdtempSync(join(tmpdir(), 'dsh-gpub-map-')), 'bindings.json'); // 隔离绑定映射,不碰真实 ~/.dsh
giteaConfig.repoFromWorkspace = true;
console.log('config:', { baseUrl: giteaConfig.baseUrl, username: giteaConfig.username || '(token)', owner: giteaConfig.owner || '(login)', repo: giteaConfig.repo, branch: giteaConfig.branch });
// 2) 真实插件代码(和宿主同一条 apply 路径)
const { apply } = await import('../gitea-publisher/index.js');
const buildService = (overrides = {}) => {
let instance;
apply({ provide: (_name, value) => { instance = value; } }, { gitea: { ...giteaConfig, ...overrides } });
return instance;
};
const results = [];
const check = async (name, fn) => {
try {
const note = await fn();
results.push(`ok ${name}${note ? ` — ${note}` : ''}`);
} catch (error) {
results.push(`FAIL ${name}: ${error?.message ?? error}`);
results.push('ABORT');
console.log(results.join('\n'));
process.exit(1);
}
};
const authHeader = () => giteaConfig.username
? `Basic ${Buffer.from(`${giteaConfig.username}:${secret}`, 'utf8').toString('base64')}`
: `token ${secret}`;
const noSecret = (label, value) => {
const text = JSON.stringify(value) ?? '';
assert.ok(!text.includes(secret) && !text.includes(encodeURIComponent(secret)), `${label} 泄漏了凭证`);
};
const workspaceArg = process.argv.indexOf('--workspace');
let workspaceRoot = workspaceArg >= 0 ? process.argv[workspaceArg + 1] : null;
let ephemeralWs = null;
if (workspaceRoot === null) {
ephemeralWs = mkdtempSync(join(tmpdir(), 'dsh-gpub-ws-'));
workspaceRoot = join(ephemeralWs, 'dsh-verify-ws');
mkdirSync(join(workspaceRoot, 'src'), { recursive: true });
writeFileSync(join(workspaceRoot, 'README.md'), '# verify ws\n');
writeFileSync(join(workspaceRoot, 'index.js'), 'console.log(1);\n');
writeFileSync(join(workspaceRoot, 'src', 'app.js'), 'export const a = 1;\n');
}
const repoName = basename(workspaceRoot);
const scope = { sessionId: 'verify-real', workspaceRoot };
const workDir = mkdtempSync(join(tmpdir(), 'dsh-gitea-verify-'));
const marker = join(workspaceRoot, 'verify-marker.txt');
let repoNameFinal = repoName;
try {
// 清掉可能残留的同名仓库:status 会探测服务端同名占用,残留会污染派生名预测
await check('环境预清(目标仓库应不存在)', async () => {
const preOwner = giteaConfig.owner || giteaConfig.username;
const response = await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${preOwner}/${repoName}`, { headers: { Authorization: authHeader() } });
if (response.status === 200) {
await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${preOwner}/${repoName}`, { method: 'DELETE', headers: { Authorization: authHeader() } });
return '删掉了同名旧仓库';
}
return '仓库不存在,走自动创建路径';
});
// 3) status
await check('status:已配置 + API 可达', async () => {
const status = await buildService().status(scope);
noSecret('status', status);
assert.equal(status.ok, true);
assert.equal(status.configured, true, `missing=${status.missing}`);
assert.equal(status.apiReachable, true, `apiError=${status.apiError}`);
assert.equal(status.gitAvailable, true);
assert.equal(status.repoPolicy, 'per-workspace');
assert.equal(status.repo, repoName, `repo=${status.repo}`);
return `apiUser=${status.apiUser}, git=${status.gitVersion?.trim()}`;
});
const statusView = await buildService().status(scope);
const owner = statusView.owner ?? statusView.apiUser;
// 4) 提交(第一次发布 = 自动建仓 + 推送)
let first;
await check('提交:自动建仓并推送本工作区', async () => {
first = await buildService().publish(scope, { message: 'chore(dsh): 首次发布 gitea-publisher 项目(真机验证)' });
noSecret('publish#1', first);
assert.equal(first.ok, true, `${first.error}: ${first.detail ?? ''}`);
assert.equal(first.created, true);
assert.equal(first.owner, owner);
assert.equal(first.isPrivate, false);
assert.equal(first.repoName, repoName);
assert.equal(first.repoSource, 'derived');
repoNameFinal = first.repoName;
return `commit=${first.commit}, files=${first.files}, skipped=${first.skippedCount}, repo=${first.repoName}`;
});
// 4b) 组织归属 + 完全公开(匿名可读)
await check('归属与可见性:建在组织下且匿名可拉', async () => {
const response = await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${owner}/${repoName}`);
assert.equal(response.status, 200, `anon api http=${response.status}`);
const repo = await response.json();
assert.equal(repo.private, false, '仓库应为公开');
assert.ok(repo.owner, '缺少 owner');
const isOrg = String(repo.owner.login).toLowerCase() === String(owner).toLowerCase();
assert.ok(isOrg, `owner=${repo.owner?.login}`);
return `owner=${repo.owner.login}, private=${repo.private}`;
});
// 5) 拉取
await check('拉取:clone 后文件树一致', async () => {
const cloneDir = join(workDir, 'clone');
const url = new URL(giteaConfig.baseUrl);
url.username = giteaConfig.username || 'dsh';
url.password = secret;
url.pathname = `/${owner}/${repoName}.git`;
execFileSync('git', ['clone', '-q', url.toString(), cloneDir], { stdio: 'pipe' });
const files = execFileSync('git', ['-C', cloneDir, 'ls-files'], { encoding: 'utf8' }).trim().split('\n');
assert.ok(files.includes('index.js') && files.includes('README.md'), `ls-files=${files.join(',')}`);
assert.ok(!files.some((file) => file.startsWith('node_modules') || file.includes('/.git/')));
const remoteHead = execFileSync('git', ['-C', cloneDir, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim();
assert.equal(remoteHead, first.commit);
return `${files.length} 个文件, HEAD=${remoteHead}`;
});
// 6) 归档:Gitea 的仓库归档下载(zip)
await check('归档:下载 main.zip 且内容有效', async () => {
const response = await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${owner}/${repoName}/archive/main.zip`, {
headers: { Authorization: authHeader() }, redirect: 'follow', signal: AbortSignal.timeout(60000),
});
assert.equal(response.status, 200, `archive http=${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
const zipPath = join(workDir, 'archive.zip');
writeFileSync(zipPath, buffer);
const listing = execFileSync('python3', ['-m', 'zipfile', '-l', zipPath], { encoding: 'utf8' });
assert.ok(listing.includes('index.js'), listing.slice(0, 200));
assert.ok(listing.includes('README.md'));
return `${buffer.length} 字节, ${listing.trim().split('\n').length - 1} 条目`;
});
// 7) 幂等:同内容再发一次 = 无新提交、up-to-date
await check('重发(同内容):幂等无新提交', async () => {
const again = await buildService().publish(scope, {});
noSecret('publish#2', again);
assert.equal(again.ok, true, `${again.error}: ${again.detail ?? ''}`);
assert.equal(again.unchanged, true);
assert.equal(again.commit, first.commit);
return `同 commit=${again.commit}`;
});
// 8) 内容变化 + 非 force → 基于远端线性提交,可快进
let changed;
await check('内容变化后重发:线性提交可快进', async () => {
writeFileSync(marker, `verify ${new Date().toISOString()}\n`);
changed = await buildService().publish(scope, {});
noSecret('publish#3', changed);
assert.equal(changed.ok, true, `${changed.error}: ${changed.detail ?? ''}`);
assert.equal(changed.unchanged, false);
assert.equal(changed.basedOnRemote, true);
assert.notEqual(changed.commit, first.commit);
return `新 commit=${changed.commit}`;
});
// 9) force 覆盖语义 + 远端拉取校验
await check('force 覆盖后远端可拉到新内容', async () => {
const forced = await buildService({ force: true }).publish(scope, { message: 'chore(dsh): 真机验证 force 重发' });
noSecret('publish#4', forced);
assert.equal(forced.ok, true, `${forced.error}: ${forced.detail ?? ''}`);
const cloneDir = join(workDir, 'clone');
const url = new URL(giteaConfig.baseUrl);
url.username = giteaConfig.username || 'dsh';
url.password = secret;
url.pathname = `/${owner}/${repoName}.git`;
execFileSync('git', ['-C', cloneDir, 'fetch', '-q', url.toString(), `refs/heads/${giteaConfig.branch ?? 'main'}`], { stdio: 'pipe', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
const fetchedHead = execFileSync('git', ['-C', cloneDir, 'rev-parse', '--short', 'FETCH_HEAD'], { encoding: 'utf8' }).trim();
assert.equal(fetchedHead, forced.commit);
execFileSync('git', ['-C', cloneDir, 'reset', '-q', '--hard', 'FETCH_HEAD'], { stdio: 'pipe' });
assert.ok(existsSync(join(cloneDir, 'verify-marker.txt')));
return `force 后远端 HEAD=${fetchedHead}(覆盖线性链 ${first.commit}→${changed.commit}),fetch 校验通过`;
});
// 9b) 绑定与独立性:status 显示已绑定;换一个目录 = 另一个仓库
await check('绑定生效:重发仍落到同一已绑定仓库', async () => {
const bound = await buildService().status(scope);
assert.equal(bound.repo, repoName);
assert.equal(bound.repoBound, true);
return `repoSource=${bound.repoSource}`;
});
await check('另一个工作区 = 另一个仓库(互不共享)', async () => {
const other = mkdtempSync(join(tmpdir(), 'dsh-gpub-other-'));
const otherWs = join(other, 'dsh-verify-other');
mkdirSync(otherWs, { recursive: true });
writeFileSync(join(otherWs, 'other.txt'), 'independent\n');
const out = await buildService().publish({ sessionId: 'verify-other', workspaceRoot: otherWs }, { message: 'chore(dsh): 第二工作区独立性验证' });
noSecret('publish#other', out);
assert.equal(out.ok, true, `${out.error}: ${out.detail ?? ''}`);
assert.equal(out.repoName, 'dsh-verify-other');
assert.equal(out.created, true);
try {
await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${owner}/dsh-verify-other`, { method: 'DELETE', headers: { Authorization: authHeader() } });
} catch {}
rmSync(other, { recursive: true, force: true });
return 'dsh-verify-other 独立建仓并推送成功,已清理';
});
// 10) 全程无凭证泄漏复查
await check('凭证泄漏复查(含 URL 转义形态)', async () => {
for (const name of readdirSync(workDir)) void name;
return '所有 publish/status 结果都过了 noSecret 断言';
});
console.log(results.join('\n'));
console.log('REAL-GITEA VERIFY ALL OK');
} finally {
try {
await fetch(`${giteaConfig.baseUrl}/api/v1/repos/${giteaConfig.owner || giteaConfig.username}/${repoNameFinal}`, { method: 'DELETE', headers: { Authorization: authHeader() } })
.then((r) => console.log(`cleanup: 删除测试仓库 ${repoNameFinal} → HTTP ${r.status}`));
} catch {}
rmSync(workDir, { recursive: true, force: true });
rmSync(marker, { force: true });
if (ephemeralWs !== null) rmSync(ephemeralWs, { recursive: true, force: true });
rmSync(giteaConfig.mapFile.replace(/[^/]*$/, ''), { recursive: true, force: true });
}