@antprofuse/saddle-islands-tools 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -10,8 +10,12 @@ python3 <frozen-package>/tools/cli/main.py backend --project <suite-root> --repo
|
|
|
10
10
|
npm run selftest --prefix <frozen-package>/tools/modules/backend
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
运行器消费已构建的可执行制品,不负责选择或安装Rust工具链。发起者为构建阶段准备框架要求的工具链及可解析的cargo/rustc入口,核版本后交接执行环境;不能依赖其他workspace的隐含PATH。
|
|
14
|
+
|
|
13
15
|
所有根、适配、协议、Service、启动 app/alias 与制品均为显式输入。适配和协议预检真实路径,拒绝未声明根、符号链接逃逸、缺配置或缺制品。项目源依据与 Islands 公开生成工具仍由项目声明;本包不复制 Islands 或 Saddle 运行时。
|
|
14
16
|
|
|
17
|
+
`--out`须位于业务仓外且最终目录尚不存在。工具自动创建缺失父目录,但拒绝复用已有最终目录(即使为空),避免混入历史证据;目录准备失败时不启动业务进程。
|
|
18
|
+
|
|
15
19
|
## 适配合同
|
|
16
20
|
|
|
17
21
|
适配模块导出 `project(projectRoot)`,返回 `scenarios`、`proto`、`rpc`、`process`、`frozen`、`inputFiles`、`database`、`databaseLogPolicy`、`prepare` 和 `execute`。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,mkdir,writeFile,readFile,rm,access} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {createExternalOutput} from './index.mjs';
|
|
7
|
+
test('fresh nested output is created; prior evidence and repository paths are rejected',async()=>{
|
|
8
|
+
const root=await mkdtemp(join(tmpdir(),'backend-output-'));
|
|
9
|
+
try {
|
|
10
|
+
const repo=join(root,'repo');await mkdir(repo);
|
|
11
|
+
const out=join(root,'missing','nested','run');assert.equal(await createExternalOutput(repo,out),out);
|
|
12
|
+
await assert.rejects(createExternalOutput(repo,out),{code:'EEXIST'});
|
|
13
|
+
await writeFile(join(out,'evidence.json'),'original');
|
|
14
|
+
await assert.rejects(createExternalOutput(repo,out),{code:'EEXIST'});
|
|
15
|
+
assert.equal(await readFile(join(out,'evidence.json'),'utf8'),'original');
|
|
16
|
+
const illegal=join(repo,'new','run');await assert.rejects(createExternalOutput(repo,illegal),/outside source repository/);
|
|
17
|
+
await assert.rejects(access(join(repo,'new')),{code:'ENOENT'});
|
|
18
|
+
const file=join(root,'file');await writeFile(file,'unchanged');
|
|
19
|
+
await assert.rejects(createExternalOutput(repo,join(file,'run')));
|
|
20
|
+
assert.equal(await readFile(file,'utf8'),'unchanged');
|
|
21
|
+
} finally {await rm(root,{recursive:true,force:true});}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('runner rejects reused output before starting RPC or product',async()=>{
|
|
25
|
+
const {run}=await import('./run.mjs');
|
|
26
|
+
const root=await mkdtemp(join(tmpdir(),'backend-preflight-'));
|
|
27
|
+
try {
|
|
28
|
+
const repo=join(root,'repo');await mkdir(repo);
|
|
29
|
+
const proto=join(repo,'input.proto');await writeFile(proto,'intentionally invalid proto');
|
|
30
|
+
const adapter=join(repo,'adapter.mjs');
|
|
31
|
+
await writeFile(adapter,`export async function project(){return {rpc:{serviceName:'Synthetic'},proto:${JSON.stringify(proto)},process:{app:'synthetic',alias:'test'},inputFiles:[${JSON.stringify(proto)}],scenarios:[{id:'one'}],prepare(){},execute(){throw Error('business must not start')}}}`);
|
|
32
|
+
const out=join(root,'existing');await mkdir(out);
|
|
33
|
+
await assert.rejects(run({projectRoot:repo,repositoryRoot:repo,adapter,out,binary:process.execPath}),{code:'EEXIST'});
|
|
34
|
+
await assert.rejects(access(join(out,'report.json')),{code:'ENOENT'});
|
|
35
|
+
} finally {await rm(root,{recursive:true,force:true});}
|
|
36
|
+
});
|
|
@@ -8,7 +8,7 @@ import {within,preflight} from './paths.mjs';
|
|
|
8
8
|
import {scenarioActions} from './actions.mjs';
|
|
9
9
|
import {mock} from './grpc.mjs';
|
|
10
10
|
import {product} from './product.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {createExternalOutput,sleep} from './util.mjs';
|
|
12
12
|
import {requestLogs} from './logs.mjs';
|
|
13
13
|
import {assertRouteRejection} from './rejection-logs.mjs';
|
|
14
14
|
import {classifyReport} from './report.mjs';
|
|
@@ -25,8 +25,8 @@ export async function run({projectRoot,repositoryRoot,adapter,out,binary,caseId,
|
|
|
25
25
|
const all=config.scenarios;assert(Array.isArray(all)&&all.length,'empty scenarios');assert.equal(new Set(all.map(s=>s.id)).size,all.length,'duplicate scene id');
|
|
26
26
|
const selected=caseId?all.filter(s=>s.id===caseId):all;assert(selected.length,'unknown case');
|
|
27
27
|
if(list){await new Promise(done=>process.stdout.write(JSON.stringify(all.map(({id,operation,basis,scope})=>({id,operation,basis,scope:scope??'business'})),null,2)+'\n',done));return;}
|
|
28
|
-
assert(binary&&out,'real binary and fresh external output required');await access(resolve(binary),constants.X_OK);await access(config.proto);const destination=
|
|
29
|
-
|
|
28
|
+
assert(binary&&out,'real binary and fresh external output required');await access(resolve(binary),constants.X_OK);await access(config.proto);const destination=await createExternalOutput(repositoryRoot,out);
|
|
29
|
+
const report={...config.frozen,tool:identity,projectInputs,scenarioSetSha256:createHash('sha256').update(JSON.stringify(all)).digest('hex'),started:new Date().toISOString(),scenarios:[],database:config.database};
|
|
30
30
|
let rpc,app,stage='startup';const abort=new AbortController();const listeners=new Map();
|
|
31
31
|
for(const signal of ['SIGINT','SIGTERM']){const handler=()=>{report.interrupted=signal;abort.abort();rpc?.release();};listeners.set(signal,handler);process.once(signal,handler);}
|
|
32
32
|
try{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
2
|
import {createServer} from 'node:net';
|
|
3
3
|
import {mkdir,open} from 'node:fs/promises';
|
|
4
|
-
import {resolve,relative} from 'node:path';
|
|
4
|
+
import {resolve,relative,dirname} from 'node:path';
|
|
5
5
|
import {setTimeout as sleep} from 'node:timers/promises';
|
|
6
6
|
export {sleep};
|
|
7
7
|
export async function port() {
|
|
@@ -22,3 +22,11 @@ export async function command(binary,args,dir,env={}) {
|
|
|
22
22
|
}
|
|
23
23
|
export async function until(fn,timeout=30000) { const end=Date.now()+timeout;let error;while(Date.now()<end){try{return await fn();}catch(e){error=e;await sleep(100);}}throw error ?? Error('readiness timeout'); }
|
|
24
24
|
export function externalOutput(project,out) { const p=resolve(out);const rel=relative(resolve(project),p);if(rel!=='..'&&!rel.startsWith('../')) throw Error('output must be outside source repository');return p; }
|
|
25
|
+
|
|
26
|
+
export async function createExternalOutput(repository,out) {
|
|
27
|
+
const destination=externalOutput(repository,out);
|
|
28
|
+
await mkdir(dirname(destination),{recursive:true});
|
|
29
|
+
// Keep the final creation exclusive: existing evidence must never be reused.
|
|
30
|
+
await mkdir(destination);
|
|
31
|
+
return destination;
|
|
32
|
+
}
|