@wenathlan/getry 2.0.150 → 2.0.152
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/docs/.gitkeep +0 -0
- package/package.json +51 -20
- package/tests/debonairlane.test.ts +407 -0
- package/tests/design.test.ts +29 -30
- package/tests/sandboxmodality.test.ts +84 -0
- package/docs/skills/gateway-deploy.md +0 -151
- package/docs/skills/gateway.md +0 -42
- package/tests/database-runtime-build.sh +0 -75
- package/tests/python-runtime-build.sh +0 -64
- package/tests/python-runtime-container.sh +0 -31
package/docs/.gitkeep
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/getry",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.152",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/wenathlan/devthink"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"ecmascript": "^5.0.0",
|
|
28
28
|
"firefox": ">=0.8.0",
|
|
29
29
|
"iojs": "^1.0.0",
|
|
30
|
-
"node": ">=26.
|
|
30
|
+
"node": ">=26.13.0 <27",
|
|
31
31
|
"npm": "^12.1.0",
|
|
32
32
|
"pnpm": "^12.6.0",
|
|
33
33
|
"teleport": ">=0.2.0",
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"yarn": "^4.17.1"
|
|
37
37
|
},
|
|
38
38
|
"type": "module",
|
|
39
|
+
"exports": {
|
|
40
|
+
"./thinking": {
|
|
41
|
+
"types": "./thinking.ts",
|
|
42
|
+
"default": "./thinking.ts"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
39
45
|
"scripts": {
|
|
40
46
|
"dev": "vite --host",
|
|
41
47
|
"build": "vite build",
|
|
@@ -47,23 +53,36 @@
|
|
|
47
53
|
"restore:entry": "node -e \"const{readFileSync}=require('node:fs');const{execSync}=require('node:child_process');const script=/script[^>]*App\\.tsx/;const triad=/family=Bricolage\\+Grotesque[\\s\\S]*Outfit[\\s\\S]*JetBrains\\+Mono/;const face=(h)=>script.test(h)&&triad.test(h);if(face(readFileSync('index.html','utf8'))){console.log('index.html already rides the source face');process.exit(0)}if(!face(execSync('git show HEAD:./index.html',{encoding:'utf8'}))){console.error('refusing: HEAD index.html is not the canonical source face yet (script App.tsx + the font triad) - land the source-face fix first; nothing was touched');process.exit(1)}execSync('git checkout -- index.html',{stdio:'inherit'});console.log('index.html restored from HEAD (the source face)')\""
|
|
48
54
|
},
|
|
49
55
|
"dependencies": {
|
|
56
|
+
"@fluentui/react-icons": "^2.0.345",
|
|
57
|
+
"@fontsource-variable/bricolage-grotesque": "^5.3.0",
|
|
58
|
+
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
59
|
+
"@fontsource-variable/outfit": "^5.3.0",
|
|
50
60
|
"@hono/node-server": "^2.1.5",
|
|
61
|
+
"@libsql/client": "^0.18.0",
|
|
51
62
|
"@prisma/adapter-libsql": "^7.10.1",
|
|
52
|
-
"
|
|
63
|
+
"@prisma/client": "^7.10.1",
|
|
64
|
+
"@wenathlan/saddle": "^2.1.6",
|
|
65
|
+
"@wenathlan/vault": "^2.0.151",
|
|
66
|
+
"hono": "^4.13.14",
|
|
53
67
|
"ink": "^7.1.1",
|
|
68
|
+
"lucide-react": "^1.66.0",
|
|
69
|
+
"motion": "^12.43.0",
|
|
70
|
+
"react": "^19.3.1",
|
|
54
71
|
"react-devtools-core": "^8.0.0",
|
|
72
|
+
"react-dom": "^19.3.1",
|
|
73
|
+
"wouter": "^3.13.0",
|
|
55
74
|
"z-ai-web-dev-sdk": "^0.0.18"
|
|
56
75
|
},
|
|
57
76
|
"devDependencies": {
|
|
58
|
-
"@biomejs/biome": "^2.6.
|
|
59
|
-
"@capacitor/android": "^8.
|
|
60
|
-
"@capacitor/cli": "^8.
|
|
61
|
-
"@capacitor/core": "^8.
|
|
62
|
-
"@capacitor/ios": "^8.
|
|
63
|
-
"@fluentui/react-icons": "^2.0.
|
|
77
|
+
"@biomejs/biome": "^2.6.4",
|
|
78
|
+
"@capacitor/android": "^8.6.0",
|
|
79
|
+
"@capacitor/cli": "^8.6.0",
|
|
80
|
+
"@capacitor/core": "^8.6.0",
|
|
81
|
+
"@capacitor/ios": "^8.6.0",
|
|
82
|
+
"@fluentui/react-icons": "^2.0.345",
|
|
64
83
|
"@hookform/resolvers": "^5.9.2",
|
|
65
84
|
"@libsql/client": "^0.18.0",
|
|
66
|
-
"@opencode-ai/sdk": "^1.19.
|
|
85
|
+
"@opencode-ai/sdk": "^1.19.5",
|
|
67
86
|
"@prisma/client": "^7.10.1",
|
|
68
87
|
"@radix-ui/react-accordion": "^1.3.1",
|
|
69
88
|
"@radix-ui/react-alert-dialog": "^1.2.1",
|
|
@@ -96,41 +115,49 @@
|
|
|
96
115
|
"@tailwindcss/vite": "^4.3.3",
|
|
97
116
|
"@tsconfig/node26": "^26.0.1",
|
|
98
117
|
"@types/better-sqlite3": "^9.6.0",
|
|
99
|
-
"@types/chrome": "^0.3.
|
|
118
|
+
"@types/chrome": "^0.3.7",
|
|
100
119
|
"@types/express": "^5.0.6",
|
|
101
|
-
"@types/
|
|
120
|
+
"@types/google.maps": "^3.66.9",
|
|
121
|
+
"@types/node": "^26.6.6",
|
|
102
122
|
"@types/react": "^19.3.0",
|
|
103
123
|
"@types/react-dom": "^19.3.0",
|
|
104
124
|
"@vitejs/plugin-react": "^6.1.2",
|
|
105
125
|
"add": "^2.0.9",
|
|
106
|
-
"autoprefixer": "^10.6.
|
|
107
|
-
"axios": "^1.
|
|
126
|
+
"autoprefixer": "^10.6.2",
|
|
127
|
+
"axios": "^1.21.0",
|
|
108
128
|
"better-sqlite3": "^13.0.3",
|
|
109
129
|
"bun-types": "^1.4.4",
|
|
130
|
+
"cheerio": "^1.2.0",
|
|
110
131
|
"class-variance-authority": "^0.7.1",
|
|
111
132
|
"clsx": "^2.1.1",
|
|
112
133
|
"cmdk": "^1.1.1",
|
|
113
134
|
"drizzle-kit": "^0.31.11",
|
|
114
135
|
"drizzle-orm": "^0.45.4",
|
|
115
136
|
"embla-carousel-react": "^8.6.0",
|
|
137
|
+
"emittery": "^2.2.0",
|
|
116
138
|
"esbuild": "^0.28.2",
|
|
117
139
|
"express": "^5.3.0",
|
|
118
140
|
"fast-check": "^4.10.2",
|
|
119
141
|
"framer-motion": "^13.5.1",
|
|
142
|
+
"hono": "^4.13.14",
|
|
120
143
|
"input-otp": "^1.5.0",
|
|
121
|
-
"
|
|
144
|
+
"jsdom": "26.1.0",
|
|
145
|
+
"lucide-react": "^1.66.0",
|
|
122
146
|
"mysql2": "^3.24.6",
|
|
123
147
|
"nanoid": "^6.0.2",
|
|
124
148
|
"next-themes": "^0.4.6",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
149
|
+
"p-limit": "^7.3.3",
|
|
150
|
+
"p-retry": "^8.0.1",
|
|
151
|
+
"playwright": "^1.64.1",
|
|
152
|
+
"pnpm": "^12.16.1",
|
|
153
|
+
"postcss": "^8.5.32",
|
|
127
154
|
"prettier": "^3.9.10",
|
|
128
155
|
"prisma": "^7.10.1",
|
|
129
156
|
"react": "^19.3.1",
|
|
130
157
|
"react-day-picker": "^10.0.2",
|
|
131
158
|
"react-dom": "^19.3.1",
|
|
132
159
|
"react-hook-form": "^7.91.0",
|
|
133
|
-
"react-resizable-panels": "^4.15.
|
|
160
|
+
"react-resizable-panels": "^4.15.3",
|
|
134
161
|
"recharts": "^3.10.1",
|
|
135
162
|
"sonner": "^2.0.8",
|
|
136
163
|
"streamdown": "^2.7.1",
|
|
@@ -143,6 +170,7 @@
|
|
|
143
170
|
"typescript": "^7.0.2",
|
|
144
171
|
"vaul": "^1.1.2",
|
|
145
172
|
"vite": "^8.3.5",
|
|
173
|
+
"vite-plugin-manus-runtime": "^0.0.59",
|
|
146
174
|
"vitest": "^5.0.5",
|
|
147
175
|
"wouter": "^3.13.0",
|
|
148
176
|
"yaml": "^2.9.1",
|
|
@@ -173,8 +201,11 @@
|
|
|
173
201
|
"proxy-addr": "2.0.8",
|
|
174
202
|
"body-parser": "^2.4.0",
|
|
175
203
|
"fast-uri": "^3.1.9",
|
|
176
|
-
"follow-redirects": "^1.16.
|
|
204
|
+
"follow-redirects": "^1.16.2",
|
|
205
|
+
"minimatch": "^11.0.3",
|
|
206
|
+
"glob": "^14.0.3",
|
|
177
207
|
"zod": "^4.6.7",
|
|
178
|
-
"undici": "^7.30.1"
|
|
208
|
+
"undici": "^7.30.1",
|
|
209
|
+
"shell-quote": "^1.11.0"
|
|
179
210
|
}
|
|
180
211
|
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// # debonairlane.test — the debonair api lane of the family gateway (the
|
|
2
|
+
// mount adapter contract): the thirteen-route catalog mirrors the debonair
|
|
3
|
+
// gateway table in the family shape (first match wins, the params capture,
|
|
4
|
+
// an empty capture never matches, the provider lanes answer absent), the
|
|
5
|
+
// adapter translates the getry mount shape (the web request) into the
|
|
6
|
+
// debonair gateway request (the json lane, the raw lane, the query record)
|
|
7
|
+
// and the debonair answer back into the web response (the json body, the
|
|
8
|
+
// blob stream with the digest headers, the standardized error shape, the
|
|
9
|
+
// 502 lane-faulted guard — never a throw), the origin config rides the
|
|
10
|
+
// vite env law under the house https gate, and the forward seat rides the
|
|
11
|
+
// wire over a fake fetch (the same routes, the body lanes, the blob
|
|
12
|
+
// headers, the error catalog, the unreachable lane). runnable with vitest:
|
|
13
|
+
// pnpm test
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { describe, it } from "vitest";
|
|
16
|
+
import {
|
|
17
|
+
DEBONAIR_URL_ENV,
|
|
18
|
+
DEBONAIRROUTES,
|
|
19
|
+
type DebonairDispatch,
|
|
20
|
+
type DebonairGatewayAnswer,
|
|
21
|
+
type DebonairGatewayRequest,
|
|
22
|
+
debonaircandidate,
|
|
23
|
+
debonairforward,
|
|
24
|
+
debonairorigin,
|
|
25
|
+
debonairrequest,
|
|
26
|
+
debonairresponse,
|
|
27
|
+
debonairserve,
|
|
28
|
+
matchdebonairpath,
|
|
29
|
+
matchdebonairroute,
|
|
30
|
+
} from "../debonairlane.ts";
|
|
31
|
+
|
|
32
|
+
/** the debonair origin the wire tests ride (a public https host — the law). */
|
|
33
|
+
const ORIGIN = "https://debonair.example.com";
|
|
34
|
+
|
|
35
|
+
/** builds one mounted web request — the getry server shape. */
|
|
36
|
+
function mount(method: string, path: string, init: { body?: string; contenttype?: string } = {}): Request {
|
|
37
|
+
const headers: Record<string, string> = {};
|
|
38
|
+
if (init.contenttype !== undefined) headers["content-type"] = init.contenttype;
|
|
39
|
+
return new Request(`${ORIGIN}${path}`, { method, headers, body: init.body });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** builds one fake debonair engine seat — records every gateway request it
|
|
43
|
+
* receives and answers the scripted answer (the value builder, the typed
|
|
44
|
+
* refusal or a fault thrown past the guards).
|
|
45
|
+
*
|
|
46
|
+
* @param serve the answer the seat serves.
|
|
47
|
+
*/
|
|
48
|
+
function engineseat(
|
|
49
|
+
serve: (request: DebonairGatewayRequest) => DebonairGatewayAnswer | Promise<DebonairGatewayAnswer>,
|
|
50
|
+
): { dispatch: DebonairDispatch; calls: DebonairGatewayRequest[] } {
|
|
51
|
+
const calls: DebonairGatewayRequest[] = [];
|
|
52
|
+
const dispatch: DebonairDispatch = async (request) => {
|
|
53
|
+
calls.push(request);
|
|
54
|
+
return serve(request);
|
|
55
|
+
};
|
|
56
|
+
return { dispatch, calls };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** one engine seat serving one ok value (the happy lane builder). */
|
|
60
|
+
function valueseat(value: (request: DebonairGatewayRequest) => unknown): {
|
|
61
|
+
dispatch: DebonairDispatch;
|
|
62
|
+
calls: DebonairGatewayRequest[];
|
|
63
|
+
} {
|
|
64
|
+
return engineseat((request) => ({ ok: true, status: 200, value: value(request) }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("the debonair route catalog (the published contract mirror)", () => {
|
|
68
|
+
it("carries the thirteen debonair routes in the family shape", () => {
|
|
69
|
+
assert.equal(DEBONAIRROUTES.length, 13);
|
|
70
|
+
assert.deepEqual(DEBONAIRROUTES[0], { method: "POST", path: "/api/v1/ingest/upload", handler: "uploadIngest" });
|
|
71
|
+
assert.deepEqual(DEBONAIRROUTES[5], { method: "GET", path: "/api/v1/blob/:id", handler: "getBlob" });
|
|
72
|
+
assert.deepEqual(DEBONAIRROUTES[12], { method: "GET", path: "/api/v1/vitrine", handler: "listVitrine" });
|
|
73
|
+
for (const route of DEBONAIRROUTES) {
|
|
74
|
+
assert.ok(route.path.startsWith("/api/v1/"), `${route.path} rides the api prefix`);
|
|
75
|
+
assert.ok(["GET", "POST", "DELETE"].includes(route.method));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("matches the params, refuses the empty capture and answers the method verdict", () => {
|
|
80
|
+
const routed = matchdebonairroute("GET", "/api/v1/ingest/jobs/123");
|
|
81
|
+
assert.ok(routed.kind === "routed");
|
|
82
|
+
assert.equal(routed.route.handler, "getIngestJob");
|
|
83
|
+
assert.deepEqual(routed.params, { id: "123" });
|
|
84
|
+
// the empty capture never matches — a route param always carries a value
|
|
85
|
+
assert.equal(matchdebonairpath("/api/v1/blob/:id", "/api/v1/blob/"), null);
|
|
86
|
+
// the specific rows ride first: the jobs row wins over the :id row
|
|
87
|
+
const shelf = matchdebonairroute("GET", "/api/v1/ingest");
|
|
88
|
+
assert.ok(shelf.kind === "routed" && shelf.route.handler === "listIngestShelf");
|
|
89
|
+
// the path matches, the method does not — the allowed list answers
|
|
90
|
+
const method = matchdebonairroute("PUT", "/api/v1/vitrine");
|
|
91
|
+
assert.ok(method.kind === "method");
|
|
92
|
+
assert.deepEqual(method.allowed, ["GET"]);
|
|
93
|
+
// the provider lanes own their paths — the catalog answers absent
|
|
94
|
+
const foreign = matchdebonairroute("POST", "/v1/chat/completions");
|
|
95
|
+
assert.deepEqual(foreign, { kind: "absent" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("decodes the captured params (the vault safeDecode law)", () => {
|
|
99
|
+
const params = matchdebonairpath("/api/v1/profiles/:genre", "/api/v1/profiles/drill%20beats");
|
|
100
|
+
assert.deepEqual(params, { genre: "drill beats" });
|
|
101
|
+
const malformed = matchdebonairpath("/api/v1/profiles/:genre", "/api/v1/profiles/%zz");
|
|
102
|
+
assert.deepEqual(malformed, { genre: "%zz" }, "a malformed escape keeps the raw text");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("the mount adapter (the getry server shape to the debonair contract)", () => {
|
|
107
|
+
it("builds the gateway request off the web request — the json lane", async () => {
|
|
108
|
+
const { dispatch, calls } = valueseat(() => ({ jobId: "42" }));
|
|
109
|
+
const response = await debonairserve(
|
|
110
|
+
mount("POST", "/api/v1/ingest/upload", {
|
|
111
|
+
body: JSON.stringify({ bytes: "QUJD", name: "take.wav", ownerId: "123" }),
|
|
112
|
+
contenttype: "application/json",
|
|
113
|
+
}),
|
|
114
|
+
dispatch,
|
|
115
|
+
);
|
|
116
|
+
assert.equal(response.status, 200);
|
|
117
|
+
assert.deepEqual(await response.json(), { jobId: "42" });
|
|
118
|
+
assert.equal(calls.length, 1);
|
|
119
|
+
const seen = calls[0];
|
|
120
|
+
assert.ok(seen);
|
|
121
|
+
assert.equal(seen.method, "POST");
|
|
122
|
+
assert.equal(seen.path, "/api/v1/ingest/upload");
|
|
123
|
+
assert.deepEqual(seen.json, { bytes: "QUJD", name: "take.wav", ownerId: "123" });
|
|
124
|
+
assert.equal(seen.raw, null);
|
|
125
|
+
assert.deepEqual(seen.query, {});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("carries the raw body verbatim with the facts in the query (the deposit law)", async () => {
|
|
129
|
+
const { dispatch, calls } = valueseat(() => ({ jobId: "43" }));
|
|
130
|
+
const response = await debonairserve(
|
|
131
|
+
mount("POST", "/api/v1/ingest/upload?name=take.wav&ownerId=123&kind=reference", {
|
|
132
|
+
body: "the verbatim bytes",
|
|
133
|
+
}),
|
|
134
|
+
dispatch,
|
|
135
|
+
);
|
|
136
|
+
assert.equal(response.status, 200);
|
|
137
|
+
const seen = calls[0];
|
|
138
|
+
assert.ok(seen);
|
|
139
|
+
assert.equal(seen.json, null);
|
|
140
|
+
assert.ok(seen.raw instanceof Uint8Array);
|
|
141
|
+
assert.equal(new TextDecoder().decode(seen.raw), "the verbatim bytes");
|
|
142
|
+
assert.deepEqual(seen.query, { name: "take.wav", ownerId: "123", kind: "reference" });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("serves the blob stream with the pointer's kind and the digest headers", async () => {
|
|
146
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
147
|
+
const pointer = {
|
|
148
|
+
id: "777",
|
|
149
|
+
bytes: bytes.byteLength,
|
|
150
|
+
sha256: "a".repeat(64),
|
|
151
|
+
kind: "audio/wav",
|
|
152
|
+
storedAt: "2026-06-01T00:00:00.000Z",
|
|
153
|
+
};
|
|
154
|
+
const { dispatch, calls } = valueseat(() => ({ bytes, mime: "audio/wav", pointer }));
|
|
155
|
+
const response = await debonairserve(mount("GET", "/api/v1/blob/777"), dispatch);
|
|
156
|
+
assert.equal(response.status, 200);
|
|
157
|
+
assert.equal(response.headers.get("content-type"), "audio/wav");
|
|
158
|
+
assert.equal(response.headers.get("x-debonair-blob-sha256"), "a".repeat(64));
|
|
159
|
+
assert.equal(response.headers.get("x-debonair-blob-bytes"), "4");
|
|
160
|
+
assert.equal(response.headers.get("x-debonair-blob-stored-at"), "2026-06-01T00:00:00.000Z");
|
|
161
|
+
assert.deepEqual(new Uint8Array(await response.arrayBuffer()), bytes);
|
|
162
|
+
assert.equal(calls[0]?.path, "/api/v1/blob/777");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("answers the standardized error shape with the debonair status", async () => {
|
|
166
|
+
const { dispatch } = engineseat(() => ({
|
|
167
|
+
ok: false,
|
|
168
|
+
status: 405,
|
|
169
|
+
error: { code: "method-not-allowed", message: "PUT does not ride /api/v1/vitrine" },
|
|
170
|
+
}));
|
|
171
|
+
const response = await debonairserve(mount("PUT", "/api/v1/vitrine"), dispatch);
|
|
172
|
+
assert.equal(response.status, 405);
|
|
173
|
+
assert.deepEqual(await response.json(), {
|
|
174
|
+
error: { code: "method-not-allowed", message: "PUT does not ride /api/v1/vitrine" },
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("guards a seat that faults past the debonair guards — never a throw", async () => {
|
|
179
|
+
const { dispatch } = engineseat(() => {
|
|
180
|
+
throw new Error("the engine faulted");
|
|
181
|
+
});
|
|
182
|
+
const response = await debonairserve(mount("GET", "/api/v1/vitrine"), dispatch);
|
|
183
|
+
assert.equal(response.status, 502);
|
|
184
|
+
const body = (await response.json()) as { error: { code: string; message: string } };
|
|
185
|
+
assert.equal(body.error.code, "lane-faulted");
|
|
186
|
+
assert.match(body.error.message, /the engine faulted/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("serves the 201 registrations and the json refusals through the answer translator", async () => {
|
|
190
|
+
const created = debonairresponse({ ok: true, status: 201, value: { jobId: "9" } });
|
|
191
|
+
assert.equal(created.status, 201);
|
|
192
|
+
assert.deepEqual(await created.json(), { jobId: "9" });
|
|
193
|
+
assert.equal(created.headers.get("content-type")?.split(";")[0], "application/json");
|
|
194
|
+
const refused = debonairresponse({
|
|
195
|
+
ok: false,
|
|
196
|
+
status: 400,
|
|
197
|
+
error: { code: "bad-request", message: "the upload needs a name" },
|
|
198
|
+
});
|
|
199
|
+
assert.equal(refused.status, 400);
|
|
200
|
+
assert.deepEqual(await refused.json(), { error: { code: "bad-request", message: "the upload needs a name" } });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("builds the gateway request alone (the translation seat the mount loop reads)", async () => {
|
|
204
|
+
const request = await debonairrequest(
|
|
205
|
+
new Request(`${ORIGIN}/api/v1/ingest?ownerId=123&ownerId=456`, { method: "GET" }),
|
|
206
|
+
);
|
|
207
|
+
assert.equal(request.method, "GET");
|
|
208
|
+
assert.equal(request.path, "/api/v1/ingest");
|
|
209
|
+
assert.deepEqual(request.query, { ownerId: "123" }, "the first value wins (the web law)");
|
|
210
|
+
assert.equal(request.json, null);
|
|
211
|
+
assert.equal(request.raw, null);
|
|
212
|
+
// a broken json body answers the null json lane — the debonair route owns the refusal
|
|
213
|
+
const broken = await debonairrequest(
|
|
214
|
+
new Request(`${ORIGIN}/api/v1/ingest/upload`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "content-type": "application/json" },
|
|
217
|
+
body: "{not json",
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
assert.equal(broken.json, null);
|
|
221
|
+
assert.equal(broken.raw, null);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("the origin config (the vite env law under the house https gate)", () => {
|
|
226
|
+
it("names the environment seat the deploy pins the origin through", () => {
|
|
227
|
+
assert.equal(DEBONAIR_URL_ENV, "VITE_DEBONAIR_URL");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("trims the candidate and refuses the unlawful origins", () => {
|
|
231
|
+
assert.equal(debonaircandidate(" https://debonair.example.com/ "), "https://debonair.example.com");
|
|
232
|
+
assert.equal(debonaircandidate(""), "", "the empty candidate keeps the lane closed");
|
|
233
|
+
assert.equal(debonaircandidate("http://debonair.example.com"), "", "http never rides");
|
|
234
|
+
assert.equal(debonaircandidate("https://localhost"), "", "loopback never rides");
|
|
235
|
+
assert.equal(debonaircandidate("https://10.0.0.1"), "", "the private range never rides");
|
|
236
|
+
assert.equal(debonaircandidate("not a url"), "", "a malformed candidate never rides");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("answers no origin when the deploy carries none (the lane stays closed)", () => {
|
|
240
|
+
assert.equal(debonairorigin(), "");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("the forward seat (the debonair engine over the wire)", () => {
|
|
245
|
+
/** one recorded wire call. */
|
|
246
|
+
interface WireCall {
|
|
247
|
+
url: string;
|
|
248
|
+
method: string;
|
|
249
|
+
contenttype: string | null;
|
|
250
|
+
body: string | null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** builds one fake fetch serving the debonair wire law. */
|
|
254
|
+
function fakewire(answer: (call: WireCall) => Response): { fetchImpl: typeof fetch; calls: WireCall[] } {
|
|
255
|
+
const calls: WireCall[] = [];
|
|
256
|
+
const fetchImpl: typeof fetch = async (input, init) => {
|
|
257
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
258
|
+
const call: WireCall = {
|
|
259
|
+
url: typeof input === "string" ? input : input.url,
|
|
260
|
+
method: init?.method ?? "GET",
|
|
261
|
+
contenttype:
|
|
262
|
+
headers !== undefined && typeof headers["content-type"] === "string" ? headers["content-type"] : null,
|
|
263
|
+
body: typeof init?.body === "string" ? init.body : null,
|
|
264
|
+
};
|
|
265
|
+
calls.push(call);
|
|
266
|
+
return answer(call);
|
|
267
|
+
};
|
|
268
|
+
return { fetchImpl, calls };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
it("forwards the json lane and parses the json answer back", async () => {
|
|
272
|
+
const { fetchImpl, calls } = fakewire(() => Response.json({ tracks: [], profiles: [] }));
|
|
273
|
+
const dispatch = debonairforward(ORIGIN, fetchImpl);
|
|
274
|
+
const answer = await dispatch({ method: "GET", path: "/api/v1/vitrine", query: {}, json: null, raw: null });
|
|
275
|
+
assert.ok(answer.ok);
|
|
276
|
+
assert.equal(answer.status, 200);
|
|
277
|
+
assert.deepEqual(answer.value, { tracks: [], profiles: [] });
|
|
278
|
+
const call = calls[0];
|
|
279
|
+
assert.ok(call);
|
|
280
|
+
assert.equal(call.method, "GET");
|
|
281
|
+
assert.equal(call.url, `${ORIGIN}/api/v1/vitrine`);
|
|
282
|
+
// the json lane rides the wire with the json content type
|
|
283
|
+
const posted = await dispatch({
|
|
284
|
+
method: "POST",
|
|
285
|
+
path: "/api/v1/profiles/mine",
|
|
286
|
+
query: {},
|
|
287
|
+
json: { genre: "trap", ownerId: "123" },
|
|
288
|
+
raw: null,
|
|
289
|
+
});
|
|
290
|
+
assert.ok(posted.ok);
|
|
291
|
+
const mine = calls[1];
|
|
292
|
+
assert.ok(mine);
|
|
293
|
+
assert.equal(mine.method, "POST");
|
|
294
|
+
assert.equal(mine.url, `${ORIGIN}/api/v1/profiles/mine`);
|
|
295
|
+
assert.equal(mine.contenttype, "application/json");
|
|
296
|
+
assert.deepEqual(JSON.parse(mine.body ?? "{}"), { genre: "trap", ownerId: "123" });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("forwards the raw lane and the query facts", async () => {
|
|
300
|
+
const { fetchImpl, calls } = fakewire(() => Response.json({ removed: true }, { status: 200 }));
|
|
301
|
+
const dispatch = debonairforward(ORIGIN, fetchImpl);
|
|
302
|
+
const answer = await dispatch({
|
|
303
|
+
method: "DELETE",
|
|
304
|
+
path: "/api/v1/ingest/123",
|
|
305
|
+
query: {},
|
|
306
|
+
json: null,
|
|
307
|
+
raw: new Uint8Array([9, 9]),
|
|
308
|
+
});
|
|
309
|
+
assert.ok(answer.ok);
|
|
310
|
+
assert.deepEqual(answer.value, { removed: true });
|
|
311
|
+
const call = calls[0];
|
|
312
|
+
assert.ok(call);
|
|
313
|
+
assert.equal(call.method, "DELETE");
|
|
314
|
+
assert.equal(call.url, `${ORIGIN}/api/v1/ingest/123`);
|
|
315
|
+
assert.equal(call.body, null, "the bytes ride the body lane, never the string lane");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("forwards the query record with the path (the deposit facts)", async () => {
|
|
319
|
+
const { fetchImpl, calls } = fakewire(() => Response.json({ jobId: "51" }, { status: 201 }));
|
|
320
|
+
const dispatch = debonairforward(ORIGIN, fetchImpl);
|
|
321
|
+
const answer = await dispatch({
|
|
322
|
+
method: "POST",
|
|
323
|
+
path: "/api/v1/ingest/upload",
|
|
324
|
+
query: { name: "take.wav", ownerId: "123", kind: "reference" },
|
|
325
|
+
json: null,
|
|
326
|
+
raw: new Uint8Array([1, 2, 3]),
|
|
327
|
+
});
|
|
328
|
+
assert.ok(answer.ok);
|
|
329
|
+
assert.equal(answer.status, 201);
|
|
330
|
+
assert.equal(
|
|
331
|
+
calls[0]?.url,
|
|
332
|
+
`${ORIGIN}/api/v1/ingest/upload?name=take.wav&ownerId=123&kind=reference`,
|
|
333
|
+
"the query facts ride the wire verbatim",
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("parses the blob stream headers back into the stream value", async () => {
|
|
338
|
+
const { fetchImpl } = fakewire(
|
|
339
|
+
() =>
|
|
340
|
+
new Response(new Uint8Array([5, 6, 7]), {
|
|
341
|
+
status: 200,
|
|
342
|
+
headers: {
|
|
343
|
+
"content-type": "audio/wav",
|
|
344
|
+
"x-debonair-blob-sha256": "b".repeat(64),
|
|
345
|
+
"x-debonair-blob-bytes": "3",
|
|
346
|
+
"x-debonair-blob-stored-at": "2026-06-02T00:00:00.000Z",
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
const dispatch = debonairforward(ORIGIN, fetchImpl);
|
|
351
|
+
const answer = await dispatch({ method: "GET", path: "/api/v1/blob/321", query: {}, json: null, raw: null });
|
|
352
|
+
assert.ok(answer.ok);
|
|
353
|
+
const stream = answer.value as { bytes: Uint8Array; mime: string; pointer: Record<string, unknown> };
|
|
354
|
+
assert.deepEqual([...stream.bytes], [5, 6, 7]);
|
|
355
|
+
assert.equal(stream.mime, "audio/wav");
|
|
356
|
+
assert.equal(stream.pointer.id, "321");
|
|
357
|
+
assert.equal(stream.pointer.bytes, 3);
|
|
358
|
+
assert.equal(stream.pointer.sha256, "b".repeat(64));
|
|
359
|
+
assert.equal(stream.pointer.storedAt, "2026-06-02T00:00:00.000Z");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("parses the standardized error shape and guards the unreachable lane", async () => {
|
|
363
|
+
const { fetchImpl } = fakewire(() =>
|
|
364
|
+
Response.json(
|
|
365
|
+
{ error: { code: "bad-owner", message: "the upload needs the owner pointer in the minted decimal form" } },
|
|
366
|
+
{ status: 400 },
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
const dispatch = debonairforward(ORIGIN, fetchImpl);
|
|
370
|
+
const refused = await dispatch({
|
|
371
|
+
method: "POST",
|
|
372
|
+
path: "/api/v1/ingest/upload",
|
|
373
|
+
query: {},
|
|
374
|
+
json: { bytes: "QQ==", name: "take.wav" },
|
|
375
|
+
raw: null,
|
|
376
|
+
});
|
|
377
|
+
assert.ok(!refused.ok);
|
|
378
|
+
assert.equal(refused.status, 400);
|
|
379
|
+
assert.equal(refused.error.code, "bad-owner");
|
|
380
|
+
const hanging: typeof fetch = () => {
|
|
381
|
+
throw new Error("the network is down");
|
|
382
|
+
};
|
|
383
|
+
const offline = await debonairforward(
|
|
384
|
+
ORIGIN,
|
|
385
|
+
hanging,
|
|
386
|
+
)({
|
|
387
|
+
method: "GET",
|
|
388
|
+
path: "/api/v1/vitrine",
|
|
389
|
+
query: {},
|
|
390
|
+
json: null,
|
|
391
|
+
raw: null,
|
|
392
|
+
});
|
|
393
|
+
assert.ok(!offline.ok);
|
|
394
|
+
assert.equal(offline.status, 502);
|
|
395
|
+
assert.equal(offline.error.code, "lane-unreachable");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("composes the whole lane: the mount request rides the forward seat and answers as the web response", async () => {
|
|
399
|
+
const { fetchImpl } = fakewire(() => Response.json({ records: [] }, { status: 200 }));
|
|
400
|
+
const response = await debonairserve(
|
|
401
|
+
mount("GET", "/api/v1/ingest?ownerId=123"),
|
|
402
|
+
debonairforward(ORIGIN, fetchImpl),
|
|
403
|
+
);
|
|
404
|
+
assert.equal(response.status, 200);
|
|
405
|
+
assert.deepEqual(await response.json(), { records: [] });
|
|
406
|
+
});
|
|
407
|
+
});
|
package/tests/design.test.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
// theme anchor, page anchors, prisma schema, db layer) and pins the reform:
|
|
6
6
|
// the devthink foundation (mica #202020 over the 8-step ladder #242424→
|
|
7
7
|
// #494949, Inter Variable + Geist Mono, the 13px/450 body, the trylle shadow
|
|
8
|
-
// ladder), the windows hands (press .7 pins, dtPopIntro, the
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// onboarding → the platform over the catalogued seen
|
|
13
|
-
// list (no pill, no all-properties transition, no
|
|
14
|
-
// law, no banned hue, no build ghost). The
|
|
15
|
-
// browser, no DOM.
|
|
8
|
+
// ladder), the windows hands (press .7 pins, dtPopIntro, the win11
|
|
9
|
+
// scrollbar), the two-accent contract (the getry violet identity beside the
|
|
10
|
+
// windows accent), the FAMILY STANDALONE LAW (the shell answers the direct
|
|
11
|
+
// url full-bleed, the window chrome lives only inside the os embed), the
|
|
12
|
+
// entry-flow law (intro → onboarding → the platform over the catalogued seen
|
|
13
|
+
// state) and the banned list (no pill, no all-properties transition, no
|
|
14
|
+
// scale(0) outside the pin law, no banned hue, no build ghost). The sources
|
|
15
|
+
// are read as text — no browser, no DOM.
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { describe, expect, it } from "vitest";
|
|
18
18
|
|
|
@@ -22,11 +22,11 @@ function read(path: string): string {
|
|
|
22
22
|
|
|
23
23
|
const CSS = read("../Sol/sol.css");
|
|
24
24
|
const ENTRY = read("../index.html");
|
|
25
|
-
const MARK = read("../Sol/
|
|
25
|
+
const MARK = read("../Sol/wingets/ShellBrandmark.tsx");
|
|
26
26
|
const SHELL = read("../Sol/shell/Shell.tsx");
|
|
27
27
|
const SOL = read("../Sol/Sol.tsx");
|
|
28
28
|
const HOME = read("../Sol/home/home.tsx");
|
|
29
|
-
const GATE = read("../Sol/
|
|
29
|
+
const GATE = read("../Sol/wingets/HomeEntrygate.tsx");
|
|
30
30
|
const INTRO = read("../Sol/intro/intro.tsx");
|
|
31
31
|
const ONBOARDING = read("../Sol/onboarding/onboarding.tsx");
|
|
32
32
|
const STATE = read("../onboardingstate.ts");
|
|
@@ -88,8 +88,9 @@ describe("design contract — the devthink foundation", () => {
|
|
|
88
88
|
expect(CSS).toContain("inset 0 8px 14px -6px rgb(255 255 255 / 22%)");
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it("keeps the two-accent contract (the
|
|
92
|
-
expect(CSS).toContain("--
|
|
91
|
+
it("keeps the two-accent contract (the white sun beside the windows accent)", () => {
|
|
92
|
+
expect(CSS).toContain("--color-signal: #ffffff");
|
|
93
|
+
expect(CSS).toContain("--violet-500: var(--color-signal)");
|
|
93
94
|
expect(CSS).toContain("--accent: var(--violet-500)");
|
|
94
95
|
expect(CSS).toContain("--win-accent: var(--color-win)");
|
|
95
96
|
expect(CSS).toContain("--color-win: #4cc2ff");
|
|
@@ -122,24 +123,22 @@ describe("design contract — the windows hands", () => {
|
|
|
122
123
|
expect(CSS).toContain("--dur-press: 100ms");
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
it("
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
expect(SHELL).toContain(
|
|
126
|
+
it("answers the direct url full-bleed — the window chrome lives only inside the os", () => {
|
|
127
|
+
/* the family standalone law: an app accessed by its own url is the
|
|
128
|
+
normal application, one hundred percent — the shell renders
|
|
129
|
+
shell--standalone with no window chrome of its own (no title bar, no
|
|
130
|
+
captions, no resize handles, no inactive law, no entry scale), while
|
|
131
|
+
the framed contract keeps the bare stage for the os power clip */
|
|
132
|
+
expect(SHELL).toContain("shell--standalone");
|
|
133
|
+
expect(SHELL).toContain("shell--embedded");
|
|
134
|
+
expect(SHELL).not.toMatch(
|
|
135
|
+
/className="appframe"|winapp__title|winapp__cap|winapp__rz|winchip|setPointerCapture|data-inactive|RESIZE_EDGES/,
|
|
136
|
+
);
|
|
137
|
+
expect(CSS).toMatch(/\.shell--standalone \{[^}]*height: 100vh/);
|
|
133
138
|
});
|
|
134
139
|
|
|
135
|
-
it("
|
|
136
|
-
expect(rules(CSS)).toMatch(/\.winapp \{[^}]*animation: winappOpen var\(--dur-2\) var\(--ease-window\)/);
|
|
140
|
+
it("rides the win11 scrollbar beside the standalone stage", () => {
|
|
137
141
|
expect(CSS).toContain("*:hover::-webkit-scrollbar-thumb");
|
|
138
|
-
expect(rules(CSS)).toMatch(/\.winapp__rz \{[^}]*touch-action: none/);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("rests the title-bar mark on the landing (logo discipline)", () => {
|
|
142
|
-
expect(CSS).toContain("data-landing");
|
|
143
142
|
});
|
|
144
143
|
|
|
145
144
|
it("pins the footer to the window floor (the sticky-footer law)", () => {
|
|
@@ -158,7 +157,7 @@ describe("design contract — the banned list", () => {
|
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
it("never ships an all-properties transition", () => {
|
|
161
|
-
expect(/transition:\s*all/.test(rules(CSS)), "transition: all
|
|
160
|
+
expect(/transition:\s*all/.test(rules(CSS)), "transition: all fails the design gate").toBe(false);
|
|
162
161
|
});
|
|
163
162
|
});
|
|
164
163
|
|
|
@@ -198,9 +197,9 @@ describe("flow contract — intro → onboarding → the platform", () => {
|
|
|
198
197
|
|
|
199
198
|
it("gates the home anchor on the seen state before anything shows", () => {
|
|
200
199
|
/* the widget surface law: the dotted widget name is written exactly
|
|
201
|
-
once (inside Sol/
|
|
200
|
+
once (inside Sol/wingets/index.ts) and the page answers the surface
|
|
202
201
|
import with its canonical alias — never the dotted path again */
|
|
203
|
-
expect(HOME).toMatch(/import \{ HomeEntrygate as EntryGate \} from "\.\.\/
|
|
202
|
+
expect(HOME).toMatch(/import \{ HomeEntrygate as EntryGate \} from "\.\.\/wingets"/);
|
|
204
203
|
expect(GATE).toContain('navigate("/intro", { replace: true })');
|
|
205
204
|
expect(GATE).toContain('navigate("/onboarding", { replace: true })');
|
|
206
205
|
expect(GATE).toContain("flowthrough");
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// # sandboxmodality.test — the workload lane declaration of getry (wave
|
|
2
|
+
// WL1): the mirrored rows answer the engine catalog byte for byte (the
|
|
3
|
+
// lockstep pin — every lane, role, demand and honest today the engine
|
|
4
|
+
// FAMILYLANES table carries for getry, compared through the library
|
|
5
|
+
// bridge so the vendor can never drift in silence), every row names the
|
|
6
|
+
// app slug, and the seat law answers the deploy's opt-in (absent or blank
|
|
7
|
+
// answers the declared local default, "join" opts the lanes onto the
|
|
8
|
+
// platform's join resolver — the nuclear modality law, and a named
|
|
9
|
+
// modality id pins the seat for the platform's join to validate at
|
|
10
|
+
// dispatch — this module never judges engine vocabulary). runnable with
|
|
11
|
+
// vitest:
|
|
12
|
+
// pnpm vitest run tests/sandboxmodality.test.ts
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { describe, it } from "vitest";
|
|
15
|
+
import { lanesof } from "@wenathlan/saddle/workloadlaw";
|
|
16
|
+
import {
|
|
17
|
+
APPSLUG,
|
|
18
|
+
SANDBOXLANEENV,
|
|
19
|
+
WORKLOADLANES,
|
|
20
|
+
laneof,
|
|
21
|
+
lanesandbox,
|
|
22
|
+
lanesandboxfromenv,
|
|
23
|
+
} from "../sandboxmodality.ts";
|
|
24
|
+
|
|
25
|
+
describe("the workload lane mirror (the engine catalog lockstep)", () => {
|
|
26
|
+
it("answers the engine catalog rows for getry — the byte pin", () => {
|
|
27
|
+
const engine = lanesof("getry");
|
|
28
|
+
assert.equal(engine.length, WORKLOADLANES.length, "the mirror carries every getry lane");
|
|
29
|
+
for (let index = 0; index < engine.length; index += 1) {
|
|
30
|
+
const row = engine[index];
|
|
31
|
+
const mirror = WORKLOADLANES[index];
|
|
32
|
+
assert.equal(mirror.app, row.app);
|
|
33
|
+
assert.equal(mirror.lane, row.lane, `lane ${index} id drifts from the engine`);
|
|
34
|
+
assert.equal(mirror.role, row.role, `lane ${row.lane} role drifts from the engine`);
|
|
35
|
+
assert.deepEqual(mirror.demand, row.demand, `lane ${row.lane} demand drifts from the engine`);
|
|
36
|
+
assert.equal(mirror.today, row.today, `lane ${row.lane} today drifts from the engine`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("carries the app slug on every row and answers the lane lookup", () => {
|
|
41
|
+
for (const row of WORKLOADLANES) {
|
|
42
|
+
assert.equal(row.app, APPSLUG);
|
|
43
|
+
}
|
|
44
|
+
const first = laneof("inference-route");
|
|
45
|
+
assert.ok(first !== undefined);
|
|
46
|
+
assert.equal(first.today, "engine-lane");
|
|
47
|
+
assert.equal(laneof("no-such-lane"), undefined);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("the sandbox lane seat (the deploy decides)", () => {
|
|
52
|
+
it("answers the declared local default when the deploy carries no choice", () => {
|
|
53
|
+
const absent = lanesandbox(undefined);
|
|
54
|
+
assert.deepEqual(absent, { ok: true, seat: "local", sandboxed: false });
|
|
55
|
+
const blank = lanesandbox(" ");
|
|
56
|
+
assert.deepEqual(blank, { ok: true, seat: "local", sandboxed: false });
|
|
57
|
+
const explicit = lanesandbox("local");
|
|
58
|
+
assert.deepEqual(explicit, { ok: true, seat: "local", sandboxed: false });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("opts the lanes onto the platform join when the deploy chooses join", () => {
|
|
62
|
+
const joined = lanesandbox("join");
|
|
63
|
+
assert.deepEqual(joined, { ok: true, seat: "join", sandboxed: true, pinned: false });
|
|
64
|
+
const padded = lanesandbox(" join ");
|
|
65
|
+
assert.deepEqual(padded, { ok: true, seat: "join", sandboxed: true, pinned: false });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("pins the named modality for the platform join to validate", () => {
|
|
69
|
+
const pinned = lanesandbox("runner-node");
|
|
70
|
+
assert.deepEqual(pinned, { ok: true, seat: "runner-node", sandboxed: true, pinned: true });
|
|
71
|
+
// the module never judges engine vocabulary: the platform's join
|
|
72
|
+
// validates the pinned id at dispatch, the mirror only names it
|
|
73
|
+
const unusual = lanesandbox("microvm-firecracker");
|
|
74
|
+
assert.deepEqual(unusual, { ok: true, seat: "microvm-firecracker", sandboxed: true, pinned: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reads the seat off the environment lookup the deploy pins", () => {
|
|
78
|
+
assert.equal(SANDBOXLANEENV, "GETRY_SANDBOX_LANE");
|
|
79
|
+
const unset = lanesandboxfromenv(() => undefined);
|
|
80
|
+
assert.deepEqual(unset, { ok: true, seat: "local", sandboxed: false });
|
|
81
|
+
const set = lanesandboxfromenv((key) => (key === SANDBOXLANEENV ? "join" : undefined));
|
|
82
|
+
assert.deepEqual(set, { ok: true, seat: "join", sandboxed: true, pinned: false });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: gateway-deploy
|
|
3
|
-
description: >-
|
|
4
|
-
SKILL oficial de deploy replicável das cópias de gateway DevThink (@devthink/ai)
|
|
5
|
-
no Z.ai. Use toda vez que abrir um chat novo e receber esta skill: baixa, descompacta,
|
|
6
|
-
coloca em src/, cadastra as 22 chaves NVIDIA no DB, faz o push e o usuário deploya
|
|
7
|
-
N cópias de gateway no Z.ai.
|
|
8
|
-
triggers:
|
|
9
|
-
- "gateway deploy"
|
|
10
|
-
- "cópia de gateway"
|
|
11
|
-
- "devthink gateway"
|
|
12
|
-
- "neozips"
|
|
13
|
-
- "chaves nvidia"
|
|
14
|
-
- "deploy z.ai"
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# SKILL — Deploy Replicável de Cópias de Gateway DevThink no Z.ai
|
|
18
|
-
|
|
19
|
-
> Leia 100% deste documento antes de agir. Nada de improvisar: o código já está
|
|
20
|
-
> pronto no repositório e no arquivo compactado. Sua tarefa é montar o ambiente,
|
|
21
|
-
> registrar as chaves, validar as rotas e fazer o push. Sem parar o serviço.
|
|
22
|
-
|
|
23
|
-
## 0. Contexto (leia em voz alta pro seu cérebro)
|
|
24
|
-
|
|
25
|
-
| Item | Valor |
|
|
26
|
-
|------|-------|
|
|
27
|
-
| Plataforma | `@devthink/ai` — gateway multi-provider Next.js 16 (App Router) |
|
|
28
|
-
| Versões de gateway | V1 (ZAI) · V2 (Babel, pausada → fallback V1) · V3 (NVIDIA NIM) · V4 (OpenCode+Kilo free) · V5 (Unified/OpenRouter) |
|
|
29
|
-
| Rotas por versão | `chat/completions` `completions` `embeddings` `keys` `messages` `models` `responses` (7 × 5 = 35 rotas) |
|
|
30
|
-
| Repo oficial | `wenathlan/gateway` (privado) — fonte da verdade, sempre `main` |
|
|
31
|
-
| Endpoints Z.ai | `https://internal-api.z.ai/v1` (SDK base) · bypass V1: headers `X-Token` + `userId` diretos |
|
|
32
|
-
| NVIDIA NIM | `https://integrate.api.nvidia.com/v1` — 22 chaves `nvapi-…` em rotação LRU |
|
|
33
|
-
| DB | SQLite via Prisma 7 + `@prisma/adapter-libsql` (adapter recebe `{ url }` direto) |
|
|
34
|
-
| Auth GitHub | Device flow `client_id 178c6fc778ccc68e1d6a` → `github.com/login/device` → token em `.github-token` |
|
|
35
|
-
| Commits | SEMPRE como iakadion `174824991+iakadion@users.noreply.github.com` |
|
|
36
|
-
|
|
37
|
-
## 1. When To Use
|
|
38
|
-
|
|
39
|
-
| Trigger | Purpose | Detail | Rule |
|
|
40
|
-
|---------|---------|--------|------|
|
|
41
|
-
| "monta a gateway" | Ambiente do zero | clone → install → db → keys → dev | nunca localhost fixo, porta 3000 da sandbox |
|
|
42
|
-
| "cadastra as chaves" | Registro NVIDIA | `bun run db:push` + `bun run keys:register` | 22 chaves ativas na tabela `ApiKey` |
|
|
43
|
-
| "repara a gateway" | Rotas quebradas | comparar com `neogateway-src/` canônico | INLINE-PER-ROUTE: cada `route.ts` é single source of truth |
|
|
44
|
-
| "nova cópia" | Réplicas no Z.ai | repita §2 na sandbox nova | cada cópia = DB próprio, mesmas rotas |
|
|
45
|
-
| "push" | Publicar | bump de versão → commit → push `main` | painéis 0/0/0, workflows verdes |
|
|
46
|
-
|
|
47
|
-
## 2. Fluxo de Montagem (executar nesta ordem EXATA)
|
|
48
|
-
|
|
49
|
-
### 2.1 — Fonte A: repositório (preferida — já está tudo pronto)
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
cd /home/z/my-project
|
|
53
|
-
git clone https://<token>@github.com/wenathlan/gateway.git gateway-clone
|
|
54
|
-
# copiar o conteúdo pronto para o projeto da sandbox
|
|
55
|
-
cp -r gateway-clone/src/app/v1 gateway-clone/src/app/v2 gateway-clone/src/app/v3 \
|
|
56
|
-
gateway-clone/src/app/v4 gateway-clone/src/app/v5 src/app/
|
|
57
|
-
cp gateway-clone/src/app/page.tsx gateway-clone/src/app/layout.tsx gateway-clone/src/app/globals.css src/app/
|
|
58
|
-
cp gateway-clone/src/lib/db.ts gateway-clone/src/lib/ui.ts src/lib/
|
|
59
|
-
cp gateway-clone/prisma/schema.prisma gateway-clone/prisma.config.ts .
|
|
60
|
-
cp -r gateway-clone/scripts .
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 2.2 — Fonte B: arquivo compactado (fallback — neozips do wormhole)
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
# 1. baixar via agent-browser (Chromium headless descriptografa sozinho via Service Worker)
|
|
67
|
-
agent-browser open "https://wormhole.app/D72BW4#YFIAupAqpmd_ILpUbYvNAA"
|
|
68
|
-
agent-browser snapshot -i # achar botão "Download file" e clicar
|
|
69
|
-
ls ~/Downloads/neozips.rar # arquivo limpo (5.9MB→3.75GB conforme o link)
|
|
70
|
-
|
|
71
|
-
# 2. extrair com UNRAR 7.x estático (unrar-free 0.3.1 NÃO suporta RAR5!)
|
|
72
|
-
curl -sL https://www.rarlab.com/rar/rarlinux-x64-712.tar.gz | tar xz
|
|
73
|
-
./rar/unrar x -o+ neozips.rar # → 7 sub-rars
|
|
74
|
-
# 3. extrair UM sub-rar por vez e APAGAR cada um após extrair (disco 10GB!)
|
|
75
|
-
# neodevthink(17MB) neopackages(50MB) neoimg(158MB) neoskills(175MB)
|
|
76
|
-
# neochatinterface(175MB) neogateway(511MB) neodocs(2.66GB — pule se apertado)
|
|
77
|
-
# 4. neogateway é um repositório git cru: materialize com
|
|
78
|
-
git --work-tree=/home/z/neo/neogateway-src checkout HEAD -- .
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 2.3 — Dependências e banco
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
bun install # ~9s
|
|
85
|
-
bun run db:generate # Prisma 7 client (driver adapters)
|
|
86
|
-
bun run db:push # cria ChatMessage, ApiKey, SessionContext, TownKey
|
|
87
|
-
bun run keys:register # registra as 22 chaves NVIDIA → "total active nvidia keys: 22"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 2.4 — Segurança das chaves
|
|
91
|
-
|
|
92
|
-
| Regra | Detalhe |
|
|
93
|
-
|-------|---------|
|
|
94
|
-
| Fonte das chaves | `nvidia/nvidia-keys.json` (22 entradas `nvapi-…`) do repo privado `wenathlan/gateway` (fonte original: histórico git do neogateway); template de formato disponível no histórico do repo privado |
|
|
95
|
-
| Repo privado | `wenathlan/gateway` é privado → chaves podem viajar nele |
|
|
96
|
-
| Repo público | NUNCA commitar chaves: `.gitignore` antes de qualquer push público |
|
|
97
|
-
| `.env` | `DATABASE_URL=file:/home/z/my-project/db/custom.db` (SQLite local) |
|
|
98
|
-
|
|
99
|
-
### 2.5 — Validação (tudo verde ou não acabou)
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ # 200
|
|
103
|
-
curl -s http://localhost:3000/v3/keys | head -c 200 # 22 chaves mascaradas
|
|
104
|
-
curl -s http://localhost:3000/v1/models | head -c 200 # devthink meta-model
|
|
105
|
-
curl -s -X POST http://localhost:3000/v1/chat/completions \
|
|
106
|
-
-H "Content-Type: application/json" \
|
|
107
|
-
-d '{"model":"glm-5.3-flash","messages":[{"role":"user","content":"ping"}],"max_tokens":20}'
|
|
108
|
-
# → resposta com usage + session_id = V1 VIVA
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## 3. Deploy de N Cópias no Z.ai
|
|
112
|
-
|
|
113
|
-
1. Push em `wenathlan/gateway` (main) com o bump de versão.
|
|
114
|
-
2. Em cada chat novo da sandbox Z.ai: cole esta skill → o agente executa §2 → cada sandbox vira uma cópia de gateway independente (DB próprio).
|
|
115
|
-
3. Réplica V1 precisa dos headers `X-Token` + `userId` (bypass do SDK compartilhado). V2–V5 funcionam fora da sandbox.
|
|
116
|
-
4. Domínios gerados: `https://<sandbox-id>.space-z.ai/` — as 35 rotas ficam disponíveis imediatamente.
|
|
117
|
-
|
|
118
|
-
## 4. Autenticação GitHub (se o token não existir)
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
# gera código de device flow (expira em 15 min — mostre ao usuário, NUNCA pare o serviço)
|
|
122
|
-
curl -s -X POST https://github.com/login/device/code -H "Accept: application/json" \
|
|
123
|
-
-d "client_id=178c6fc778ccc68e1d6a&scope=repo,workflow,read:org,user"
|
|
124
|
-
# usuário autoriza em https://github.com/login/device
|
|
125
|
-
# poller troca device_code por access_token → salva em .github-token (chmod 600)
|
|
126
|
-
# regra 45: NUNCA re-pollar após AUTHORIZED
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## 5. Checklist Final (10 pontos)
|
|
130
|
-
|
|
131
|
-
- [ ] 35 rotas V1–V5 presentes em `src/app/v{1..5}/`
|
|
132
|
-
- [ ] `prisma/schema.prisma` com ChatMessage + ApiKey + SessionContext + TownKey
|
|
133
|
-
- [ ] `bun run db:push` verde
|
|
134
|
-
- [ ] `bun run keys:register` → 22 chaves ativas
|
|
135
|
-
- [ ] `/v1/chat/completions` responde com `session_id` + `usage`
|
|
136
|
-
- [ ] `/v3/keys` lista 22 chaves mascaradas
|
|
137
|
-
- [ ] `.github-token` com chmod 600 (não commitar!)
|
|
138
|
-
- [ ] Commits assinados como iakadion
|
|
139
|
-
- [ ] Worklog do sandbox atualizado (`worklog.md` — append, nunca overwrite)
|
|
140
|
-
- [ ] Push em `wenathlan/gateway` main → usuário deploya as cópias
|
|
141
|
-
|
|
142
|
-
## 6. Armadilhas conhecidas (leia 2x)
|
|
143
|
-
|
|
144
|
-
1. **unrar-free 0.3.1 não abre RAR5** — use o binário estático 7.12 da rarlab.
|
|
145
|
-
2. **Processos background morrem entre tool calls** — use double-fork: `( setsid nohup CMD & )` → PPID 1.
|
|
146
|
-
3. **wormhole expira em 24h/100 downloads** — prefira Fonte A (repo).
|
|
147
|
-
4. **Prisma `latest` = 8.0.0-rc** — PIN em `7.10.0` (tag `prev`).
|
|
148
|
-
5. **Modelos NVIDIA EOL** — 410 Gone; liste com `GET /v1/models` e use nomes completos (`deepseek-ai/deepseek-v4.1-flash`).
|
|
149
|
-
6. **Babel.town 503** — V2 deve cair para V1 automaticamente.
|
|
150
|
-
7. **Streaming V3** — SessionStreamManager + circuit breaker 8 err/60s; timeout do cliente ≥ 120s.
|
|
151
|
-
8. **`.next` grande** — apague antes de builds pesados se o disco estiver >80%.
|
package/docs/skills/gateway.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: gateway
|
|
3
|
-
description: Publish the Getry gateway and the family deployable clones in any sandbox, starting from the release asset archive
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Gateway skill
|
|
7
|
-
|
|
8
|
-
This skill publishes the gateway application (Getry) and serves the three deployable clones the same way (vault, forge, foundry). The flow is deterministic: the same steps, in the same order, on every publication.
|
|
9
|
-
|
|
10
|
-
## Models
|
|
11
|
-
|
|
12
|
-
| Model | Where it runs | How it is born |
|
|
13
|
-
| --- | --- | --- |
|
|
14
|
-
| Getry (standard) | inside the Z.AI sandbox | the release asset archive, already in Next shape with src |
|
|
15
|
-
| Getry Next bypass | outside Z.AI (Vercel, Netlify, GitHub Pages, Caddy or any host) | the workflow Next conversion over the single Getry folder — no sandbox SDK, no captcha, no session, no chat ID, no dist folder, no public folder and none of the folders the platform usually creates; it is not a new application and does not duplicate a folder |
|
|
16
|
-
|
|
17
|
-
## The four next shapes of every site
|
|
18
|
-
|
|
19
|
-
The release assets carry the complete site archive plus FOUR next archives of every site, each packed as tar.xz at the maximum xz level with the Brotli overlay riding on top. Try them in this order:
|
|
20
|
-
|
|
21
|
-
| Shape | What it is | When to use it |
|
|
22
|
-
| --- | --- | --- |
|
|
23
|
-
| next personalized | the house tree one hundred percent personalized: no src, the app/ bridge at the root beside the theme folder | first choice — try it before anything else, it is already in the assets |
|
|
24
|
-
| next zai | the standard template the Z.AI platform accepts, riding the gateway skill, the prisma schema and the deploy map | when the platform refuses the personalized root |
|
|
25
|
-
| next fifty | the same standard shape with the forbidden folders blocked (no public, no dist, no assets, no build, no deploy) and the deploy reading the root | when the platform wants the standard shape but must never carry the forbidden folders |
|
|
26
|
-
| next normal | the plain standard template with no folder blocking: the Next defaults answer every platform | last resort — the platforms that accept nothing custom |
|
|
27
|
-
|
|
28
|
-
Every shape carries the platform file set at its root (.npmrc, .nvmrc, biome, tsconfig, vercel, netlify, wrangler, the dev vars example, the manifest), and every shape must serve any deploy platform.
|
|
29
|
-
|
|
30
|
-
## The four steps
|
|
31
|
-
|
|
32
|
-
1. Download the asset archive — pick the shape by the order above (the personalized first, then the zai, then the fifty, then the normal). For the Z.AI sandbox use the next archive of the gateway; the other platforms read the same archives, because the conversion already puts the project in src.
|
|
33
|
-
2. Extract the archive at the sandbox root — nothing may stay nested: the package.json must sit at the root.
|
|
34
|
-
3. Push to the DB — with the Prisma schema in hand, run the migration (for example, `bunx prisma db push`) and confirm the tables; the builds and the data of the person stay recorded in this DB, and the backups rise to the DB sites and to the DB+sandbox site.
|
|
35
|
-
4. Build the Next — install the dependencies locked in the lockfile and run the build; the publish button of the platform finishes with the name the person chooses.
|
|
36
|
-
|
|
37
|
-
## Flow rules
|
|
38
|
-
|
|
39
|
-
- Nothing is hardcoded: no host, no credential, no key in the code; every configuration comes from environment variables (the .dev.vars file in wrangler dev, and the host variables in the deploy).
|
|
40
|
-
- The src folder is the only accepted exception to the root without src, and it exists because Next demands it; the house tree (correlated logic at the root, the theme folder with a single CSS beside the App.tsx anchor, the pages as folders with loose components) keeps holding inside it.
|
|
41
|
-
- The personalized shape inverts the exception: the app/ directory sits at the root and the src folder does not exist.
|
|
42
|
-
- Each sandbox publishes its own way, and the communication between the sites uses mime-types and HTTPS.
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")/../.zscripts" && pwd)"
|
|
6
|
-
TEST_ROOT="$(mktemp -d)"
|
|
7
|
-
trap 'rm -rf "$TEST_ROOT"' EXIT
|
|
8
|
-
|
|
9
|
-
FAKE_BIN="$TEST_ROOT/bin"
|
|
10
|
-
mkdir -p "$FAKE_BIN"
|
|
11
|
-
cat >"$FAKE_BIN/bun" <<'EOF'
|
|
12
|
-
#!/bin/bash
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
if [ "$#" -ne 2 ] || [ "$1" != "run" ] || [ "$2" != "db:push" ]; then
|
|
16
|
-
echo "unexpected bun invocation: $*" >&2
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
case "${DATABASE_URL:-}" in
|
|
21
|
-
file:*) db_path="${DATABASE_URL#file:}" ;;
|
|
22
|
-
*)
|
|
23
|
-
echo "DATABASE_URL must be an absolute SQLite file URL" >&2
|
|
24
|
-
exit 1
|
|
25
|
-
;;
|
|
26
|
-
esac
|
|
27
|
-
|
|
28
|
-
case "$db_path" in
|
|
29
|
-
/*) ;;
|
|
30
|
-
*)
|
|
31
|
-
echo "database path must be absolute: $db_path" >&2
|
|
32
|
-
exit 1
|
|
33
|
-
;;
|
|
34
|
-
esac
|
|
35
|
-
|
|
36
|
-
mkdir -p "$(dirname "$db_path")"
|
|
37
|
-
if [ ! -f "$db_path" ]; then
|
|
38
|
-
printf 'initialized\n' >"$db_path"
|
|
39
|
-
fi
|
|
40
|
-
printf '%s\n' "$DATABASE_URL" >>"${DB_PUSH_CALLS:?}"
|
|
41
|
-
EOF
|
|
42
|
-
chmod +x "$FAKE_BIN/bun"
|
|
43
|
-
|
|
44
|
-
export PATH="$FAKE_BIN:$PATH"
|
|
45
|
-
export DB_PUSH_CALLS="$TEST_ROOT/db-push-calls"
|
|
46
|
-
|
|
47
|
-
# 没有 Preview 数据库时,应只在部署产物中初始化空库,不修改项目目录。
|
|
48
|
-
EMPTY_PROJECT="$TEST_ROOT/empty-project"
|
|
49
|
-
EMPTY_BUILD="$TEST_ROOT/empty-build"
|
|
50
|
-
mkdir -p "$EMPTY_PROJECT"
|
|
51
|
-
|
|
52
|
-
PROJECT_DIR="$EMPTY_PROJECT" BUILD_DIR="$EMPTY_BUILD" \
|
|
53
|
-
bash "$SCRIPT_DIR/database-runtime-build.sh"
|
|
54
|
-
|
|
55
|
-
test -f "$EMPTY_BUILD/db/custom.db"
|
|
56
|
-
test "$(cat "$EMPTY_BUILD/db/custom.db")" = "initialized"
|
|
57
|
-
test ! -e "$EMPTY_PROJECT/db/custom.db"
|
|
58
|
-
|
|
59
|
-
# 有 Preview 数据库时,应保留数据和同目录文件,再对产物执行 schema 同步。
|
|
60
|
-
EXISTING_PROJECT="$TEST_ROOT/existing-project"
|
|
61
|
-
EXISTING_BUILD="$TEST_ROOT/existing-build"
|
|
62
|
-
mkdir -p "$EXISTING_PROJECT/db"
|
|
63
|
-
printf 'preview-data\n' >"$EXISTING_PROJECT/db/custom.db"
|
|
64
|
-
printf 'sidecar\n' >"$EXISTING_PROJECT/db/README.txt"
|
|
65
|
-
|
|
66
|
-
PROJECT_DIR="$EXISTING_PROJECT" BUILD_DIR="$EXISTING_BUILD" \
|
|
67
|
-
bash "$SCRIPT_DIR/database-runtime-build.sh"
|
|
68
|
-
|
|
69
|
-
test "$(cat "$EXISTING_BUILD/db/custom.db")" = "preview-data"
|
|
70
|
-
test "$(cat "$EXISTING_BUILD/db/README.txt")" = "sidecar"
|
|
71
|
-
test "$(wc -l <"$DB_PUSH_CALLS" | tr -d ' ')" = "2"
|
|
72
|
-
grep -Fx "file:$EMPTY_BUILD/db/custom.db" "$DB_PUSH_CALLS"
|
|
73
|
-
grep -Fx "file:$EXISTING_BUILD/db/custom.db" "$DB_PUSH_CALLS"
|
|
74
|
-
|
|
75
|
-
echo "database runtime build tests passed"
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")/../.zscripts" && pwd)"
|
|
6
|
-
TEST_ROOT="$(mktemp -d)"
|
|
7
|
-
trap 'rm -rf "$TEST_ROOT"' EXIT
|
|
8
|
-
|
|
9
|
-
PROJECT_DIR="$TEST_ROOT/project"
|
|
10
|
-
BUILD_DIR="$TEST_ROOT/build"
|
|
11
|
-
mkdir -p \
|
|
12
|
-
"$PROJECT_DIR/scripts" \
|
|
13
|
-
"$PROJECT_DIR/.venv" \
|
|
14
|
-
"$BUILD_DIR/next-service-dist"
|
|
15
|
-
|
|
16
|
-
cat >"$PROJECT_DIR/requirements.txt" <<'EOF'
|
|
17
|
-
EOF
|
|
18
|
-
cat >"$PROJECT_DIR/scripts/report.py" <<'EOF'
|
|
19
|
-
print("report")
|
|
20
|
-
EOF
|
|
21
|
-
cat >"$PROJECT_DIR/.venv/should-not-copy.py" <<'EOF'
|
|
22
|
-
ignored
|
|
23
|
-
EOF
|
|
24
|
-
|
|
25
|
-
PROJECT_DIR="$PROJECT_DIR" BUILD_DIR="$BUILD_DIR" \
|
|
26
|
-
bash "$SCRIPT_DIR/python-runtime-build.sh"
|
|
27
|
-
|
|
28
|
-
test -f "$BUILD_DIR/next-service-dist/scripts/report.py"
|
|
29
|
-
test ! -e "$BUILD_DIR/next-service-dist/.venv/should-not-copy.py"
|
|
30
|
-
|
|
31
|
-
PYPROJECT_DIR="$TEST_ROOT/pyproject-app"
|
|
32
|
-
PYPROJECT_BUILD="$TEST_ROOT/pyproject-build"
|
|
33
|
-
mkdir -p "$PYPROJECT_DIR" "$PYPROJECT_BUILD/next-service-dist"
|
|
34
|
-
cat >"$PYPROJECT_DIR/pyproject.toml" <<'EOF'
|
|
35
|
-
[project]
|
|
36
|
-
name = "deploy-test"
|
|
37
|
-
version = "0.1.0"
|
|
38
|
-
requires-python = ">=3.12"
|
|
39
|
-
dependencies = []
|
|
40
|
-
EOF
|
|
41
|
-
cat >"$PYPROJECT_DIR/app.py" <<'EOF'
|
|
42
|
-
print("pyproject app")
|
|
43
|
-
EOF
|
|
44
|
-
|
|
45
|
-
PROJECT_DIR="$PYPROJECT_DIR" BUILD_DIR="$PYPROJECT_BUILD" \
|
|
46
|
-
bash "$SCRIPT_DIR/python-runtime-build.sh"
|
|
47
|
-
test -f "$PYPROJECT_BUILD/next-service-dist/app.py"
|
|
48
|
-
test -f "$PYPROJECT_BUILD/python-runtime/requirements.txt"
|
|
49
|
-
|
|
50
|
-
NODE_ONLY_PROJECT="$TEST_ROOT/node-only"
|
|
51
|
-
NODE_ONLY_BUILD="$TEST_ROOT/node-only-build"
|
|
52
|
-
mkdir -p "$NODE_ONLY_PROJECT/mini-services/python-worker" "$NODE_ONLY_BUILD"
|
|
53
|
-
cat >"$NODE_ONLY_PROJECT/package.json" <<'EOF'
|
|
54
|
-
{"name": "node-only"}
|
|
55
|
-
EOF
|
|
56
|
-
cat >"$NODE_ONLY_PROJECT/mini-services/python-worker/main.py" <<'EOF'
|
|
57
|
-
print("unsupported preview-only service")
|
|
58
|
-
EOF
|
|
59
|
-
|
|
60
|
-
PROJECT_DIR="$NODE_ONLY_PROJECT" BUILD_DIR="$NODE_ONLY_BUILD" \
|
|
61
|
-
bash "$SCRIPT_DIR/python-runtime-build.sh"
|
|
62
|
-
test ! -e "$NODE_ONLY_BUILD/python-runtime"
|
|
63
|
-
|
|
64
|
-
echo "python runtime build tests passed"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
RUNNER_IMAGE="${RUNNER_IMAGE:-z-ai-python-deploy-runner:test}"
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")/../.zscripts" && pwd)"
|
|
7
|
-
TEST_ROOT="$(mktemp -d)"
|
|
8
|
-
trap 'rm -rf "$TEST_ROOT"' EXIT
|
|
9
|
-
|
|
10
|
-
PROJECT_DIR="$TEST_ROOT/project"
|
|
11
|
-
BUILD_DIR="$TEST_ROOT/build"
|
|
12
|
-
mkdir -p "$PROJECT_DIR" "$BUILD_DIR/next-service-dist"
|
|
13
|
-
|
|
14
|
-
cat >"$PROJECT_DIR/requirements.txt" <<'EOF'
|
|
15
|
-
idna==3.10
|
|
16
|
-
EOF
|
|
17
|
-
cat >"$PROJECT_DIR/check_runtime.py" <<'EOF'
|
|
18
|
-
import idna
|
|
19
|
-
|
|
20
|
-
assert idna.__version__ == "3.10"
|
|
21
|
-
print("python artifact import passed")
|
|
22
|
-
EOF
|
|
23
|
-
|
|
24
|
-
PROJECT_DIR="$PROJECT_DIR" BUILD_DIR="$BUILD_DIR" \
|
|
25
|
-
bash "$SCRIPT_DIR/python-runtime-build.sh"
|
|
26
|
-
|
|
27
|
-
docker run --rm \
|
|
28
|
-
--entrypoint sh \
|
|
29
|
-
-v "$BUILD_DIR:/app" \
|
|
30
|
-
"$RUNNER_IMAGE" \
|
|
31
|
-
-c 'PYTHONPATH=/app/python-runtime/site-packages:/app/next-service-dist python /app/next-service-dist/check_runtime.py'
|