修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)
一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。
二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。
三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
This commit is contained in:
1 parent
e03465c398
commit
237a09a5b0
161 files changed
+19429
-2
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const script = fileURLToPath(new URL('../src/cli.ts', import.meta.url));
|
||||
const run = (args: string[]) => new Promise<{ code: number; output: string }>(resolve => {
|
||||
execFile(process.execPath, [script, 'configure', ...args], { encoding: 'utf8' },
|
||||
(error, stdout, stderr) => resolve({ code: error ? 1 : 0, output: stdout + stderr }));
|
||||
});
|
||||
test('真实 CLI:创建、局部修改、幂等与只读预览', async t => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'credential-configure-')); t.after(() => rm(dir, { recursive: true, force: true }));
|
||||
const file = path.join(dir, 'service.credential.json');
|
||||
const create = ['--manifest', file, '--id', 'sample-skill', '--label', '服务凭据', '--credential', 'sample/service/default'];
|
||||
assert.equal((await run(create)).code, 0);
|
||||
const first = await readFile(file, 'utf8');
|
||||
assert.equal((await run(create)).code, 0); assert.equal(await readFile(file, 'utf8'), first);
|
||||
assert.equal((await run(['--manifest', file, '--title', '连接服务', '--placeholder', '输入访问凭据'])).code, 0);
|
||||
const updated = JSON.parse(await readFile(file, 'utf8'));
|
||||
assert.equal(updated.credential, 'sample/service/default'); assert.equal(updated.ui.title, '连接服务');
|
||||
const before = await readFile(file, 'utf8');
|
||||
assert.equal((await run(['--manifest', file, '--label', '预览', '--dry-run'])).code, 0);
|
||||
assert.equal(await readFile(file, 'utf8'), before);
|
||||
});
|
||||
test('真实 CLI:拒绝更换已有身份、未知字段和不完整声明', async t => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'credential-configure-')); t.after(() => rm(dir, { recursive: true, force: true }));
|
||||
const file = path.join(dir, 'service.credential.json');
|
||||
await run(['--manifest', file, '--id', 'sample-skill', '--label', '服务凭据', '--credential', 'sample/service/default']);
|
||||
const before = await readFile(file, 'utf8');
|
||||
assert.equal((await run(['--manifest', file, '--credential', 'other/account'])).code, 1);
|
||||
assert.equal((await run(['--manifest', file, '--api-key', 'FAKE_VALUE_FOR_TEST_ONLY'])).code, 1);
|
||||
assert.equal(await readFile(file, 'utf8'), before);
|
||||
assert.equal((await run(['--manifest', path.join(dir, 'incomplete.json'), '--label', '缺字段'])).code, 1);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { request as httpRequest } from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createStore, loadManifest, type CredentialBackend } from '../src/config.ts';
|
||||
import { startServer } from '../src/server.ts';
|
||||
|
||||
const manifest = await loadManifest(fileURLToPath(new URL('../manifests/default.json', import.meta.url)));
|
||||
const fake = 'TEST_ONLY_NOT_A_REAL_SECRET_12345';
|
||||
test('通用页面默认使用中性色,不引入未经配置的品牌色', async () => {
|
||||
const css = await readFile(new URL('../public/style.css', import.meta.url), 'utf8');
|
||||
for (const match of css.matchAll(/#([0-9a-f]{6})(?:[0-9a-f]{2})?\b/gi)) {
|
||||
const color = match[1];
|
||||
assert.equal(color.slice(0, 2), color.slice(2, 4));
|
||||
assert.equal(color.slice(2, 4), color.slice(4, 6));
|
||||
}
|
||||
});
|
||||
function memory(initial?: string) {
|
||||
let value = initial;
|
||||
let writes = 0;
|
||||
const backend: CredentialBackend = { name: '测试凭据库', get: async () => value,
|
||||
set: async v => { value = v; writes++; }, delete: async () => { value = undefined; } };
|
||||
return { backend, value: () => value, writes: () => writes };
|
||||
}
|
||||
test('首次保存只写入凭据后端;状态与结果不包含密钥', async () => {
|
||||
const m = memory();
|
||||
const store = createStore(manifest, m.backend);
|
||||
const before = await store.status();
|
||||
assert.equal(before.configured, false);
|
||||
const saved = await store.save({ revision: before.revision, value: fake });
|
||||
assert.equal(m.value(), fake);
|
||||
const after = await store.status();
|
||||
assert.equal(after.configured, true);
|
||||
assert.equal(JSON.stringify({ saved, after }).includes(fake), false);
|
||||
assert.equal('target' in after, false);
|
||||
});
|
||||
test('已有值需要确认替换,旧版本请求不能覆盖新值', async () => {
|
||||
const m = memory('TEST_OLD');
|
||||
const store = createStore(manifest, m.backend);
|
||||
const before = await store.status();
|
||||
await assert.rejects(store.save({ revision: before.revision, value: fake }), /确认替换/);
|
||||
assert.equal(m.writes(), 0);
|
||||
await store.save({ revision: before.revision, value: fake, replaceExisting: true });
|
||||
await assert.rejects(store.save({ revision: before.revision, value: 'TEST_STALE', replaceExisting: true }), /发生变化/);
|
||||
assert.equal(m.value(), fake);
|
||||
});
|
||||
test('凭据服务不可用时明确失败,不回退文件,不泄漏底层错误', async () => {
|
||||
const bad: CredentialBackend = { name: '不可用', get: async () => { throw Error(fake); },
|
||||
set: async () => { throw Error(fake); }, delete: async () => {} };
|
||||
await assert.rejects(createStore(manifest, bad).status(), e => e instanceof Error && !e.message.includes(fake) && /系统凭据/.test(e.message));
|
||||
const m = memory(); m.backend.set = async () => { throw Error(fake); };
|
||||
const store = createStore(manifest, m.backend);
|
||||
await assert.rejects(store.save({ revision: (await store.status()).revision, value: fake }), e => e instanceof Error && !e.message.includes(fake));
|
||||
});
|
||||
test('拒绝路径、旧 JSON 请求、空值、多行及超长密钥', async () => {
|
||||
const m = memory(); const store = createStore(manifest, m.backend);
|
||||
const revision = (await store.status()).revision;
|
||||
for (const input of [{ revision, value: fake, target: '/tmp/unwanted.json' },
|
||||
{ revision, values: { api_key: fake } }, { revision, value: '' },
|
||||
{ revision, value: 'one\ntwo' }, { revision, value: 'x'.repeat(2501) }])
|
||||
await assert.rejects(store.save(input));
|
||||
assert.equal(m.writes(), 0);
|
||||
});
|
||||
test('HTTP 认证、Host 和跨站保护、脱敏及默认单字段占位框', async t => {
|
||||
const m = memory(); const emitted: object[] = [];
|
||||
const app = await startServer({ manifest, backend: m.backend, onComplete: v => emitted.push(v) });
|
||||
t.after(app.close);
|
||||
assert.equal((await fetch(app.origin + '/api/meta')).status, 401);
|
||||
const wrongHost = await new Promise<number | undefined>((resolve, reject) => {
|
||||
const req = httpRequest(app.origin, { headers: { Host: 'attacker.example' } }, res => { res.resume(); resolve(res.statusCode); });
|
||||
req.on('error', reject); req.end();
|
||||
});
|
||||
assert.equal(wrongHost, 403);
|
||||
const html = await fetch(app.origin);
|
||||
assert.match(html.headers.get('Content-Security-Policy')!, /frame-ancestors 'none'/);
|
||||
assert.equal((await html.text()).match(/<input\b/g)?.length, 1);
|
||||
const headers: Record<string, string> = { Origin: app.origin, 'X-Local-Request': '1', 'Content-Type': 'application/json', Authorization: 'Bearer ' + app.bootstrap };
|
||||
const auth = await fetch(app.origin + '/api/session', { method: 'POST', headers, body: '{}' });
|
||||
assert.equal(auth.status, 200); headers.Cookie = auth.headers.get('set-cookie')!.split(';')[0];
|
||||
const meta = await (await fetch(app.origin + '/api/meta', { headers })).json();
|
||||
assert.equal(meta.label, manifest.label);
|
||||
assert.deepEqual(meta.ui, manifest.ui);
|
||||
const payload = { revision: meta.revision, value: fake };
|
||||
const post = (extras: Record<string, string>) => fetch(app.origin + '/api/save', { method: 'POST', headers: { ...headers, ...extras }, body: JSON.stringify(payload) });
|
||||
assert.equal((await post({ Origin: 'https://attacker.example' })).status, 403);
|
||||
assert.equal((await post({ 'X-Local-Request': '' })).status, 403);
|
||||
const result = await post({}); assert.equal(result.status, 200);
|
||||
const visible = [await result.text(), JSON.stringify(emitted)];
|
||||
visible.push(await (await fetch(app.origin + '/api/meta', { headers })).text());
|
||||
const agent = await (await fetch(app.origin + '/agent/status', { headers })).json();
|
||||
assert.equal(agent.status, 'saved'); visible.push(JSON.stringify(agent));
|
||||
assert.equal(visible.some(v => v.includes(fake)), false);
|
||||
assert.equal((await post({})).status, 409); assert.equal(m.writes(), 1);
|
||||
});
|
||||
test('取消不写凭据,取消后不能再保存', async t => {
|
||||
const m = memory(); const app = await startServer({ manifest, backend: m.backend }); t.after(app.close);
|
||||
const headers: Record<string, string> = { Origin: app.origin, 'X-Local-Request': '1', 'Content-Type': 'application/json', Authorization: 'Bearer ' + app.bootstrap };
|
||||
const auth = await fetch(app.origin + '/api/session', { method: 'POST', headers, body: '{}' });
|
||||
headers.Cookie = auth.headers.get('set-cookie')!.split(';')[0];
|
||||
assert.equal((await fetch(app.origin + '/api/cancel', { method: 'POST', headers, body: '{}' })).status, 200);
|
||||
assert.equal((await fetch(app.origin + '/api/save', { method: 'POST', headers, body: '{}' })).status, 409);
|
||||
assert.equal(m.writes(), 0);
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { createStore, type CredentialBackend, type Manifest } from '../src/config.ts';
|
||||
import { startServer } from '../src/server.ts';
|
||||
|
||||
const manifest = (name: string): Manifest => ({ version: 1, id: 'sample-skill', label: name, credential: 'sample-skill/' + name });
|
||||
function memory() {
|
||||
const values = new Map<string, string>();
|
||||
const backend = (ref: string): CredentialBackend => ({
|
||||
name: '测试凭据库', get: async () => values.get(ref),
|
||||
set: async value => { values.set(ref, value); }, delete: async () => { values.delete(ref); }
|
||||
});
|
||||
return { values, backend };
|
||||
}
|
||||
test('同一个 Skill 的多个 key 独立保存、替换和删除', async () => {
|
||||
const m = memory(), a = manifest('first'), b = manifest('second');
|
||||
const sa = createStore(a, m.backend(a.credential)), sb = createStore(b, m.backend(b.credential));
|
||||
await sa.save({ value: 'FAKE_FIRST_VALUE', revision: (await sa.status()).revision });
|
||||
await sb.save({ value: 'FAKE_SECOND_VALUE', revision: (await sb.status()).revision });
|
||||
await sa.save({ value: 'FAKE_REPLACED_VALUE', revision: (await sa.status()).revision, replaceExisting: true });
|
||||
assert.equal(m.values.get(b.credential), 'FAKE_SECOND_VALUE');
|
||||
await m.backend(a.credential).delete();
|
||||
assert.equal((await sa.status()).configured, false); assert.equal((await sb.status()).configured, true);
|
||||
});
|
||||
test('多个端口共享浏览器 cookie 容器时,会话仍互不覆盖', async t => {
|
||||
const m = memory(), a = manifest('first'), b = manifest('second');
|
||||
const one = await startServer({ manifest: a, backend: m.backend(a.credential) });
|
||||
const two = await startServer({ manifest: b, backend: m.backend(b.credential) });
|
||||
t.after(one.close); t.after(two.close);
|
||||
const jar = new Map<string, string>();
|
||||
for (const app of [one, two]) {
|
||||
const auth = await fetch(app.origin + '/api/session', { method: 'POST',
|
||||
headers: { Origin: app.origin, Authorization: 'Bearer ' + app.bootstrap, 'X-Local-Request': '1' } });
|
||||
assert.equal(auth.status, 200);
|
||||
const cookie = auth.headers.get('set-cookie')!.split(';')[0], split = cookie.indexOf('=');
|
||||
jar.set(cookie.slice(0, split), cookie.slice(split + 1));
|
||||
}
|
||||
assert.equal(jar.size, 2);
|
||||
const cookie = [...jar].map(([k, v]) => k + '=' + v).join('; ');
|
||||
for (const [app, ref, value] of [[one, a.credential, 'FAKE_FIRST_VALUE'], [two, b.credential, 'FAKE_SECOND_VALUE']] as const) {
|
||||
const headers = { Cookie: cookie, Origin: app.origin, 'X-Local-Request': '1', 'Content-Type': 'application/json' };
|
||||
const response = await fetch(app.origin + '/api/meta', { headers }); assert.equal(response.status, 200);
|
||||
const meta = await response.json();
|
||||
const saved = await fetch(app.origin + '/api/save', { method: 'POST', headers, body: JSON.stringify({ revision: meta.revision, value }) });
|
||||
assert.equal(saved.status, 200);
|
||||
const status = await (await fetch(app.origin + '/agent/status', { headers: { Authorization: 'Bearer ' + app.bootstrap } })).json();
|
||||
assert.equal(status.credential, ref); assert.equal(status.status, 'saved');
|
||||
}
|
||||
assert.equal(m.values.size, 2);
|
||||
assert.equal((await fetch(one.origin + '/agent/status', { headers: { Authorization: 'Bearer ' + two.bootstrap } })).status, 401);
|
||||
});
|
||||
test('同一凭据的旧会话在其他会话保存后不能覆盖', async () => {
|
||||
const m = memory(), a = manifest('shared');
|
||||
const first = createStore(a, m.backend(a.credential)), second = createStore(a, m.backend(a.credential));
|
||||
const initial = await second.status();
|
||||
await first.save({ revision: (await first.status()).revision, value: 'FAKE_LATEST_VALUE' });
|
||||
await assert.rejects(second.save({ revision: initial.revision, value: 'FAKE_STALE_VALUE', replaceExisting: true }), /发生变化/);
|
||||
assert.equal(m.values.get(a.credential), 'FAKE_LATEST_VALUE');
|
||||
});
|
||||
|
||||
async function session(app: Awaited<ReturnType<typeof startServer>>) {
|
||||
const headers: Record<string, string> = { Origin: app.origin, Authorization: 'Bearer ' + app.bootstrap,
|
||||
'X-Local-Request': '1', 'Content-Type': 'application/json' };
|
||||
const auth = await fetch(app.origin + '/api/session', { method: 'POST', headers });
|
||||
headers.Cookie = auth.headers.get('set-cookie')!.split(';')[0];
|
||||
return {
|
||||
meta: async () => (await fetch(app.origin + '/api/meta', { headers })).json(),
|
||||
save: (entries: object[]) => fetch(app.origin + '/api/save', { method: 'POST', headers, body: JSON.stringify({ entries }) }),
|
||||
agent: async () => (await fetch(app.origin + '/agent/status', { headers })).json(),
|
||||
};
|
||||
}
|
||||
test('同页多 key 真实 HTTP 保存、文案与状态脱敏', async t => {
|
||||
const m = memory(), fields = [manifest('one'), manifest('two')], emitted: object[] = [];
|
||||
const app = await startServer({ manifests: fields, backends: fields.map(field => m.backend(field.credential)),
|
||||
ui: { title: '连接服务', label: '双服务配置', saveLabel: '确认保存' }, onComplete: value => emitted.push(value) });
|
||||
t.after(app.close); const client = await session(app); const meta = await client.meta();
|
||||
assert.equal(meta.fields.length, 2); assert.equal(meta.page.title, '连接服务');
|
||||
const response = await client.save(meta.fields.map((field: any, index: number) => ({ credential: field.credential, revision: field.revision, value: 'FAKE_BATCH_' + index })));
|
||||
assert.equal(response.status, 200); const result = await response.json(); assert.equal(result.status, 'saved');
|
||||
assert.equal(m.values.get(fields[0].credential), 'FAKE_BATCH_0'); assert.equal(m.values.get(fields[1].credential), 'FAKE_BATCH_1');
|
||||
assert.equal(JSON.stringify([result, await client.meta(), await client.agent(), emitted]).includes('FAKE_BATCH_'), false);
|
||||
});
|
||||
test('整组预检拒绝无效项、重复项、越界引用、未确认替换和缺失项,零写入', async t => {
|
||||
const m = memory(), fields = [manifest('one'), manifest('two')];
|
||||
m.values.set(fields[0].credential, 'FAKE_OLD');
|
||||
const app = await startServer({ manifests: fields, backends: fields.map(field => m.backend(field.credential)) });
|
||||
t.after(app.close); const client = await session(app); const meta = await client.meta();
|
||||
const entries = meta.fields.map((field: any) => ({ credential: field.credential, revision: field.revision, value: 'FAKE_NEW', replaceExisting: true }));
|
||||
for (const invalid of [
|
||||
[entries[0]], [entries[0], { ...entries[1], value: 'multi\nline' }],
|
||||
[entries[0], entries[0]], [entries[0], { ...entries[1], credential: 'not/in/page' }],
|
||||
[{ ...entries[0], replaceExisting: false }, entries[1]],
|
||||
[entries[0], { ...entries[1], revision: 'stale' }],
|
||||
[entries[0], { ...entries[1], target: '/tmp/plaintext.json' }],
|
||||
]) { assert.ok((await client.save(invalid)).status >= 400); assert.deepEqual([...m.values], [[fields[0].credential, 'FAKE_OLD']]); }
|
||||
});
|
||||
test('已有项留空不覆盖,中途失败保留已保存项并可仅重试失败项', async t => {
|
||||
const m = memory(), fields = [manifest('one'), manifest('two'), manifest('three'), manifest('four')];
|
||||
m.values.set(fields[0].credential, 'FAKE_KEEP');
|
||||
const backends = fields.map(field => m.backend(field.credential));
|
||||
let fail = true; const original = backends[2].set;
|
||||
backends[2].set = async value => { if (fail) throw Error(value); await original(value); };
|
||||
const app = await startServer({ manifests: fields, backends }); t.after(app.close);
|
||||
const client = await session(app); let meta = await client.meta();
|
||||
const response = await client.save(meta.fields.slice(1).map((field: any) => ({ credential: field.credential, revision: field.revision, value: 'FAKE_BATCH' })));
|
||||
const result = await response.json(); assert.equal(result.status, 'partial');
|
||||
assert.deepEqual(result.results.map((item: any) => item.status), ['saved', 'failed', 'not_attempted']);
|
||||
assert.equal(m.values.has(fields[3].credential), false);
|
||||
assert.equal((await client.agent()).status, 'partial'); assert.equal(JSON.stringify(result).includes('FAKE_BATCH'), false);
|
||||
assert.equal(m.values.get(fields[0].credential), 'FAKE_KEEP'); assert.equal(m.values.get(fields[1].credential), 'FAKE_BATCH');
|
||||
fail = false; meta = await client.meta();
|
||||
const retry = await client.save(meta.fields.slice(2).map((field: any) => ({ credential: field.credential, revision: field.revision, value: 'FAKE_RETRY' })));
|
||||
assert.equal((await retry.json()).status, 'saved'); assert.equal(m.values.get(fields[1].credential), 'FAKE_BATCH');
|
||||
});
|
||||
test('16 项预检并行读取,写入仍顺序执行且每项写前复验', async t => {
|
||||
const fields = Array.from({ length: 16 }, (_, index) => manifest('key-' + index));
|
||||
let active = 0, peak = 0, reads = 0, writes = 0;
|
||||
const backends = fields.map((): CredentialBackend => ({ name: '测试凭据库',
|
||||
get: async () => {
|
||||
reads++; active++; peak = Math.max(peak, active);
|
||||
await new Promise(resolve => setTimeout(resolve, 2)); active--; return undefined;
|
||||
},
|
||||
set: async () => { assert.equal(active, 0); writes++; }, delete: async () => {},
|
||||
}));
|
||||
const app = await startServer({ manifests: fields, backends }); t.after(app.close);
|
||||
const client = await session(app); const meta = await client.meta();
|
||||
peak = 0; reads = 0;
|
||||
const response = await client.save(meta.fields.map((field: any) => ({ credential: field.credential, revision: field.revision, value: 'FAKE_ONLY' })));
|
||||
assert.equal((await response.json()).status, 'saved');
|
||||
assert.equal(peak, 16); assert.equal(reads, 32); assert.equal(writes, 16);
|
||||
});
|
||||
test('页面拒绝重复引用、超限字段和空配置', async () => {
|
||||
const m = memory(), field = manifest('one');
|
||||
for (const fields of [[], [field, field], Array.from({ length: 17 }, (_, index) => manifest('key-' + index))])
|
||||
await assert.rejects(startServer({ manifests: fields, backends: fields.map(item => m.backend(item.credential)) }));
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { nativeBackend, type CredentialBackend } from '../src/config.ts';
|
||||
import { startServer } from '../src/server.ts';
|
||||
|
||||
const namespace = 'test/' + randomUUID();
|
||||
const refs = [namespace + '/first', namespace + '/second'];
|
||||
const entries: { backend: CredentialBackend; owned: boolean }[] = [];
|
||||
const directory = await mkdtemp(path.join(tmpdir(), 'credential-native-'));
|
||||
let phase = '初始化';
|
||||
let cleanupFailed = false;
|
||||
let app: Awaited<ReturnType<typeof startServer>> | undefined;
|
||||
try {
|
||||
for (const ref of refs) {
|
||||
const backend = await nativeBackend(ref);
|
||||
if (await backend.get() !== undefined) throw Error();
|
||||
entries.push({ backend, owned: false });
|
||||
}
|
||||
const values = ['TEST_ONLY_A_' + randomUUID(), 'TEST_ONLY_B_' + randomUUID()];
|
||||
phase = '同页 HTTP 多 key 保存与回读';
|
||||
app = await startServer({ manifests: refs.map((credential, index) => ({ version: 1, id: 'sample-skill', label: '测试服务' + index, credential })) });
|
||||
const headers: Record<string, string> = { Origin: app.origin, Authorization: 'Bearer ' + app.bootstrap, 'X-Local-Request': '1', 'Content-Type': 'application/json' };
|
||||
const session = await fetch(app.origin + '/api/session', { method: 'POST', headers });
|
||||
headers.Cookie = session.headers.get('set-cookie')!.split(';')[0];
|
||||
const metadata = await (await fetch(app.origin + '/api/meta', { headers })).json();
|
||||
entries.forEach(entry => { entry.owned = true; });
|
||||
const saved = await fetch(app.origin + '/api/save', { method: 'POST', headers, body: JSON.stringify({
|
||||
entries: metadata.fields.map((field: { credential: string; revision: string }, index: number) => ({ credential: field.credential, revision: field.revision, value: values[index] }))
|
||||
}) });
|
||||
if (!saved.ok || (await saved.json()).status !== 'saved') throw Error();
|
||||
for (let i = 0; i < entries.length; i++) if (await entries[i].backend.get() !== values[i]) throw Error();
|
||||
phase = '替换隔离';
|
||||
values[0] = 'TEST_ONLY_A_REPLACED_' + randomUUID();
|
||||
await entries[0].backend.set(values[0]);
|
||||
if (await entries[1].backend.get() !== values[1]) throw Error();
|
||||
phase = '多 key 业务读取';
|
||||
const files = [path.join(directory, 'first.json'), path.join(directory, 'second.json')];
|
||||
for (let i = 0; i < files.length; i++)
|
||||
await writeFile(files[i], JSON.stringify({ version: 1, id: 'sample-skill', label: '测试服务', credential: refs[i] }));
|
||||
const script = fileURLToPath(new URL('../src/run.ts', import.meta.url));
|
||||
await new Promise<void>((resolve, reject) => execFile(process.execPath, [script,
|
||||
'--manifest', files[0], '--env', 'FIRST_API_KEY', '--manifest', files[1], '--env', 'SECOND_API_KEY', '--',
|
||||
process.execPath, '-e', 'process.exit(process.env.FIRST_API_KEY?.startsWith("TEST_ONLY_A_REPLACED_") && process.env.SECOND_API_KEY?.startsWith("TEST_ONLY_B_") ? 0 : 1)'],
|
||||
{ timeout: 20_000 }, error => error ? reject(Error()) : resolve()));
|
||||
phase = '删除隔离';
|
||||
await entries[0].backend.delete();
|
||||
if (await entries[0].backend.get() !== undefined || await entries[1].backend.get() !== values[1]) throw Error();
|
||||
process.stdout.write('系统凭据库多 key 保存、替换隔离、业务读取和删除隔离验证通过(假凭据)。\n');
|
||||
} catch {
|
||||
process.stderr.write('系统凭据库验证未通过,阶段:' + phase + '。\n'); process.exitCode = 1;
|
||||
} finally {
|
||||
app?.close();
|
||||
for (const entry of entries) {
|
||||
if (!entry.owned) continue;
|
||||
try {
|
||||
if (await entry.backend.get() !== undefined) await entry.backend.delete();
|
||||
if (await entry.backend.get() !== undefined) cleanupFailed = true;
|
||||
} catch { cleanupFailed = true; }
|
||||
}
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
if (cleanupFailed) {
|
||||
process.stderr.write('测试凭据清理失败,测试引用前缀:' + namespace + '\n'); process.exitCode = 1;
|
||||
} else process.stdout.write('测试凭据和临时声明已清理。\n');
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadPage } from '../src/page.ts';
|
||||
|
||||
test('页面配置真实 CLI:相对路径、独立字段文案、更新与只读预览', async t => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'credential-page-')); t.after(() => rm(dir, { recursive: true, force: true }));
|
||||
const files = ['one', 'two'].map(name => path.join(dir, name + '.json'));
|
||||
for (const [index, file] of files.entries()) await writeFile(file, JSON.stringify({ version: 1, id: 'sample-skill', label: '服务' + index,
|
||||
credential: 'sample/' + index, ui: { placeholder: '请输入测试凭据' + index } }));
|
||||
const page = path.join(dir, 'page.json');
|
||||
const script = fileURLToPath(new URL('../src/cli.ts', import.meta.url));
|
||||
const run = (args: string[]) => new Promise<number>(resolve => execFile(process.execPath,
|
||||
[script, 'configure-page', '--page', page, ...args], error => resolve(error ? 1 : 0)));
|
||||
const args = ['--manifest', files[0], '--manifest', files[1], '--title', '两项凭据'];
|
||||
assert.equal(await run(args), 0);
|
||||
assert.deepEqual(JSON.parse(await readFile(page, 'utf8')).manifests, ['one.json', 'two.json']);
|
||||
const loaded = await loadPage(page); assert.equal(loaded.manifests.length, 2); assert.equal(loaded.ui.title, '两项凭据');
|
||||
assert.equal(loaded.manifests[1].ui?.placeholder, '请输入测试凭据1');
|
||||
assert.equal(await run(['--label', '业务配置']), 0);
|
||||
const before = await readFile(page, 'utf8');
|
||||
assert.equal(await run(['--title', '只读预览', '--dry-run']), 0); assert.equal(await readFile(page, 'utf8'), before);
|
||||
assert.notEqual(await run(['--manifest', files[0], '--manifest', files[0]]), 0);
|
||||
assert.notEqual(await run(['--api-key', 'FAKE_ONLY']), 0);
|
||||
assert.equal(await readFile(page, 'utf8'), before);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadProfile, prepareProfile, profileStatus } from '../src/profile.ts';
|
||||
import { loadManifest, type CredentialBackend } from '../src/config.ts';
|
||||
import { startServer } from '../src/server.ts';
|
||||
|
||||
test('真实分发配置:正式页面保存后可被对应业务环境读取,状态与参数不含值', async () => {
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const config = JSON.parse(await readFile(path.join(root, 'manifests/profiles.json'), 'utf8'));
|
||||
for (const name of Object.keys(config.profiles)) {
|
||||
const bindings = await loadProfile(name);
|
||||
const manifests = await Promise.all(bindings.map(b => loadManifest(b.manifest)));
|
||||
const values = new Map<string, string>();
|
||||
const backendFactory = async (ref: string): Promise<CredentialBackend> => ({
|
||||
name: 'fake', get: async () => values.get(ref),
|
||||
set: async value => { values.set(ref, value); }, delete: async () => { values.delete(ref); },
|
||||
});
|
||||
const app = await startServer({ manifests, backends: await Promise.all(manifests.map(m => backendFactory(m.credential))) });
|
||||
try {
|
||||
const headers: Record<string, string> = { Origin: app.origin, Authorization: 'Bearer ' + app.bootstrap, 'X-Local-Request': '1', 'Content-Type': 'application/json' };
|
||||
const session = await fetch(app.origin + '/api/session', { method: 'POST', headers });
|
||||
headers.Cookie = session.headers.get('set-cookie')!.split(';')[0];
|
||||
assert.equal((await fetch(app.origin)).status, 200);
|
||||
const meta = await (await fetch(app.origin + '/api/meta', { headers })).json();
|
||||
const saved = await fetch(app.origin + '/api/save', { method: 'POST', headers, body: JSON.stringify({ entries: meta.fields.map((f: { credential: string; revision: string }, i: number) => ({ credential: f.credential, revision: f.revision, value: 'TEST_ONLY_PROFILE_' + i })) }) });
|
||||
assert.equal((await saved.json()).status, 'saved');
|
||||
const status = await profileStatus(bindings, {}, async ref => values.get(ref));
|
||||
assert.equal(status.configured, true);
|
||||
assert.equal(JSON.stringify(status).includes('TEST_ONLY'), false);
|
||||
const plan = await prepareProfile(bindings, ['business-program', '--input', 'a b'], {}, async ref => values.get(ref));
|
||||
for (const b of bindings) assert.match(plan.env[b.env]!, /^TEST_ONLY_PROFILE_/);
|
||||
assert.equal(JSON.stringify(plan.args).includes('TEST_ONLY'), false);
|
||||
assert.deepEqual(plan.args, ['--input', 'a b']);
|
||||
} finally { app.close(); }
|
||||
}
|
||||
});
|
||||
|
||||
test('已有环境凭据无需读取系统库;缺失或后端失败时不启动业务', async () => {
|
||||
const bindings = await loadProfile('default');
|
||||
const env = Object.fromEntries(bindings.map(b => [b.env, 'TEST_ONLY_ENV']));
|
||||
const noRead = async () => { throw Error('TEST_ONLY_FAILURE'); };
|
||||
assert.equal((await profileStatus(bindings, env, noRead)).configured, true);
|
||||
assert.equal((await prepareProfile(bindings, ['business'], env, noRead)).env[bindings[0].env], 'TEST_ONLY_ENV');
|
||||
await assert.rejects(prepareProfile(bindings, ['business'], {}, async () => undefined), /未配置/);
|
||||
await assert.rejects(prepareProfile(bindings, ['business'], {}, noRead), error => error instanceof Error && !error.message.includes('TEST_ONLY'));
|
||||
});
|
||||
|
||||
test('配置拒绝跨目录声明、危险变量与未知业务', async t => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'credential-profile-')); t.after(() => rm(dir, { recursive: true, force: true }));
|
||||
await mkdir(path.join(dir, 'manifests'));
|
||||
for (const binding of [{ manifest: '../outside.json', env: 'API_KEY' }, { manifest: 'default.json', env: 'NODE_OPTIONS' }]) {
|
||||
await writeFile(path.join(dir, 'manifests/profiles.json'), JSON.stringify({ version: 1, profiles: { default: [binding] } }));
|
||||
await assert.rejects(loadProfile('default', dir));
|
||||
}
|
||||
await assert.rejects(loadProfile('unknown', dir));
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { parseBindings, prepareCommand } from '../src/run.ts';
|
||||
|
||||
async function fixture() {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'credential-run-'));
|
||||
const files = [path.join(dir, 'first.json'), path.join(dir, 'second.json')];
|
||||
await Promise.all(files.map((file, i) => writeFile(file, JSON.stringify({ version: 1, id: 'sample-skill', label: '服务', credential: 'sample/key-' + i }))));
|
||||
return { dir, files, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
||||
}
|
||||
test('多个 key 同时注入一个真实子进程,命令参数不包含值', async t => {
|
||||
const f = await fixture(); t.after(f.cleanup);
|
||||
const plan = await prepareCommand(['--manifest', f.files[0], '--env', 'FIRST_API_KEY', '--manifest', f.files[1], '--env', 'SECOND_API_KEY', '--',
|
||||
process.execPath, '-e', 'process.exit(process.env.FIRST_API_KEY && process.env.SECOND_API_KEY && process.env.FIRST_API_KEY !== process.env.SECOND_API_KEY ? 0 : 1)'],
|
||||
async ref => ref.endsWith('0') ? 'FAKE_FIRST_VALUE' : 'FAKE_SECOND_VALUE');
|
||||
assert.equal(plan.env.FIRST_API_KEY, 'FAKE_FIRST_VALUE'); assert.equal(plan.env.SECOND_API_KEY, 'FAKE_SECOND_VALUE');
|
||||
assert.equal(JSON.stringify(plan.args).includes('FAKE_'), false);
|
||||
await new Promise<void>((resolve, reject) => execFile(plan.command, plan.args, { env: plan.env }, error => error ? reject(error) : resolve()));
|
||||
});
|
||||
test('任一 key 缺失或后端失败,不返回可启动的命令,也不修改父环境', async t => {
|
||||
const f = await fixture(); t.after(f.cleanup);
|
||||
const args = ['--manifest', f.files[0], '--env', 'FIRST_API_KEY', '--manifest', f.files[1], '--env', 'SECOND_API_KEY', '--', 'unused'];
|
||||
const base = { KEEP: 'unchanged' };
|
||||
await assert.rejects(prepareCommand(args, async ref => ref.endsWith('0') ? 'FAKE_FIRST_VALUE' : undefined, base), /未配置/);
|
||||
await assert.rejects(prepareCommand(args, async () => { throw Error('FAKE_VALUE_MUST_NOT_LEAK'); }, base),
|
||||
error => error instanceof Error && !error.message.includes('FAKE_'));
|
||||
assert.deepEqual(base, { KEEP: 'unchanged' });
|
||||
});
|
||||
test('拒绝重复变量和不完整绑定,兼容单 key 的原入口', () => {
|
||||
assert.throws(() => parseBindings(['--env', 'DUP_KEY', '--env', 'DUP_KEY', '--', 'unused']), /重复/);
|
||||
assert.throws(() => parseBindings(['--manifest', 'a.json', '--manifest', 'b.json', '--env', 'A_KEY', '--', 'unused']));
|
||||
assert.throws(() => parseBindings(['--env', 'HOME', '--', 'unused']));
|
||||
assert.equal(parseBindings(['--env', 'SERVICE_API_KEY', '--', 'program', 'arg']).bindings.length, 1);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
// 运行实际构建产物的 DOM 契约测试,不启动浏览器,不接触真实凭据。
|
||||
class Element {
|
||||
children: Element[] = [];
|
||||
value = ''; textContent = ''; hidden = false; disabled = false; type = ''; required = false; placeholder = '';
|
||||
className = ''; id = ''; htmlFor = '';
|
||||
listeners: Record<string, (event: object) => unknown> = {};
|
||||
classes = new Set<string>();
|
||||
classList = { toggle: (key: string, enabled: boolean) => enabled ? this.classes.add(key) : this.classes.delete(key) };
|
||||
append(...children: Element[]) { this.children.push(...children); }
|
||||
replaceChildren() { this.children = []; }
|
||||
setAttribute() {}
|
||||
addEventListener(event: string, callback: (event: object) => unknown) { this.listeners[event] = callback; }
|
||||
}
|
||||
async function harness(configured = false, fail = false) {
|
||||
const nodes = Object.fromEntries(['credential-form', 'fields', 'save', 'heading', 'context', 'hint', 'message'].map(key => [key, new Element()]));
|
||||
const fields = [0, 1].map(index => ({ id: 'sample', label: index ? '<img src=x>' : '语音服务', credential: 'sample/' + index,
|
||||
configured: index === 0 && configured, revision: 'revision-' + index, storage: '测试凭据库', ui: { placeholder: '测试输入' } }));
|
||||
const metadata = { fields, page: { title: '连接服务', label: '两个服务', saveLabel: '确认保存' }, outcome: 'waiting' };
|
||||
const submitted: any[] = [];
|
||||
const lifecycle: Record<string, () => void> = {};
|
||||
const context = { document: { getElementById: (id: string) => nodes[id], createElement: () => new Element(), title: '' },
|
||||
location: { hash: '', pathname: '/' }, history: { replaceState() {} }, AbortSignal,
|
||||
window: { addEventListener: (key: string, callback: () => void) => { lifecycle[key] = callback; } },
|
||||
fetch: async (url: string, options: any) => {
|
||||
if (url === '/api/meta') return { ok: true, json: async () => structuredClone(metadata) };
|
||||
assert.equal(url, '/api/save'); submitted.push(JSON.parse(options.body));
|
||||
if (fail) {
|
||||
metadata.fields[0].configured = true; metadata.outcome = 'partial';
|
||||
return { ok: true, json: async () => ({ status: 'partial', results: [{ credential: 'sample/0', status: 'saved' }, { credential: 'sample/1', status: 'failed' }] }) };
|
||||
}
|
||||
metadata.outcome = 'saved'; return { ok: true, json: async () => ({ status: 'saved' }) };
|
||||
} };
|
||||
runInNewContext(await readFile(new URL('../public/app.js', import.meta.url), 'utf8'), context);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const inputs = () => nodes.fields.children.map(field => field.children[1]);
|
||||
const input = () => nodes['credential-form'].listeners.input({});
|
||||
const submit = () => nodes['credential-form'].listeners.submit({ preventDefault() {} });
|
||||
return { nodes, inputs, input, submit, submitted, lifecycle };
|
||||
}
|
||||
test('真实前端产物:配置生成两个密码框、纯文本标签、必填与成功清空', async () => {
|
||||
const h = await harness();
|
||||
assert.equal(h.inputs().length, 2); assert.ok(h.inputs().every(input => input.type === 'password' && input.required));
|
||||
assert.equal(h.nodes.fields.children[1].children[0].textContent, '<img src=x>');
|
||||
assert.equal(h.nodes.heading.textContent, '连接服务');
|
||||
h.inputs()[0].value = 'FAKE_ONE'; h.input(); assert.equal(h.nodes.save.disabled, true);
|
||||
h.inputs()[1].value = 'FAKE_TWO'; h.input(); assert.equal(h.nodes.save.disabled, false);
|
||||
await h.submit(); assert.equal(h.submitted[0].entries.length, 2);
|
||||
assert.ok(h.inputs().every(input => !input.value)); assert.equal(h.nodes['credential-form'].hidden, true);
|
||||
});
|
||||
test('真实前端产物:已有项留空保留,替换按钮明确,离开清空', async () => {
|
||||
const h = await harness(true);
|
||||
assert.equal(h.inputs()[0].required, false); assert.match(h.inputs()[0].placeholder, /留空保留/);
|
||||
h.inputs()[0].value = 'FAKE_REPLACE'; h.input(); assert.equal(h.nodes.save.textContent, '替换并保存');
|
||||
h.inputs()[0].value = ''; h.inputs()[1].value = 'FAKE_SECOND'; h.input();
|
||||
await h.submit(); assert.equal(h.submitted[0].entries.length, 1); assert.equal(h.submitted[0].entries[0].credential, 'sample/1');
|
||||
h.inputs()[0].value = 'FAKE_LEAVE'; h.lifecycle.pagehide(); assert.equal(h.inputs()[0].value, '');
|
||||
});
|
||||
test('真实前端产物:部分失败不显示全部保存,刷新状态并允许补填', async () => {
|
||||
const h = await harness(false, true);
|
||||
h.inputs().forEach(input => { input.value = 'FAKE_ONLY'; }); h.input(); await h.submit();
|
||||
assert.equal(h.nodes['credential-form'].hidden, false); assert.match(h.nodes.message.textContent, /未确认成功/);
|
||||
assert.ok(h.inputs().every(input => !input.value)); assert.equal(h.inputs()[0].required, false);
|
||||
assert.equal(h.inputs()[1].required, true); assert.equal(h.nodes.save.disabled, true);
|
||||
h.inputs()[1].value = 'FAKE_RETRY'; h.input(); assert.equal(h.nodes.save.disabled, false);
|
||||
});
|
||||
Reference in new issue
Block a user