@sun-asterisk/sungen 3.1.2-beta.102 → 3.1.2-beta.104
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/dist/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +22 -20
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +19 -16
- package/package.json +2 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
- package/src/orchestrator/templates/specs-api.ts +19 -16
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"AAmDA,cAAM,SAAS;IACb,OAAO,CAAC,OAAO,CAA8C;IAE7D,OAAO,CAAC,GAAG;IAWX,kHAAkH;IAC5G,IAAI,CACR,KAAK,EAAE,MAAM,EACb,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EAC1E,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC/B,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CA+BxF;AAED,eAAO,MAAM,GAAG,WAAkB,CAAC"}
|
|
@@ -48,6 +48,7 @@ exports.api = void 0;
|
|
|
48
48
|
*/
|
|
49
49
|
const fs = __importStar(require("fs"));
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
|
+
const test_1 = require("@playwright/test");
|
|
51
52
|
function loadEnvQa() {
|
|
52
53
|
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
53
54
|
const p = path.join(process.cwd(), name);
|
|
@@ -95,33 +96,34 @@ class ApiClient {
|
|
|
95
96
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
96
97
|
if (!base)
|
|
97
98
|
throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
98
|
-
const
|
|
99
|
-
let body;
|
|
99
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
100
100
|
const headers = { ...(conf.headers || {}) };
|
|
101
|
+
let data; // JSON body — Playwright serializes + sets content-type
|
|
101
102
|
if (req.body !== undefined && req.body !== null) {
|
|
102
|
-
|
|
103
|
-
body = JSON.stringify(resolved);
|
|
104
|
-
if (!headers['content-type'] && !headers['Content-Type'])
|
|
105
|
-
headers['content-type'] = 'application/json';
|
|
103
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
106
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
107
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
108
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
109
|
+
const ctx = await test_1.request.newContext({
|
|
110
|
+
baseURL: base,
|
|
111
|
+
extraHTTPHeaders: headers,
|
|
112
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
113
|
+
});
|
|
110
114
|
try {
|
|
111
|
-
res = await fetch(
|
|
115
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
116
|
+
const text = await res.text();
|
|
117
|
+
let parsed = text;
|
|
118
|
+
try {
|
|
119
|
+
parsed = text ? JSON.parse(text) : null;
|
|
120
|
+
}
|
|
121
|
+
catch { /* non-JSON → keep text */ }
|
|
122
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
112
123
|
}
|
|
113
124
|
finally {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const text = await res.text();
|
|
117
|
-
let parsed = text;
|
|
118
|
-
try {
|
|
119
|
-
parsed = text ? JSON.parse(text) : null;
|
|
125
|
+
await ctx.dispose();
|
|
120
126
|
}
|
|
121
|
-
catch { /* non-JSON → keep text */ }
|
|
122
|
-
const outHeaders = {};
|
|
123
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
124
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
exports.api = new ApiClient();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oBAAoB;AACpB;;;;;;;;;;GAUG;AACH,uCAAyB;AACzB,2CAA6B;
|
|
1
|
+
{"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { request, type APIRequestContext } from '@playwright/test';
|
|
15
16
|
|
|
16
17
|
interface ApiDataSource {
|
|
17
18
|
kind?: string;
|
|
@@ -71,30 +72,32 @@ class ApiClient {
|
|
|
71
72
|
const { conf } = this.cfg(req.datasource);
|
|
72
73
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
73
74
|
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
74
|
-
const
|
|
75
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
75
76
|
|
|
76
|
-
let body: string | undefined;
|
|
77
77
|
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
78
|
+
let data: any; // JSON body — Playwright serializes + sets content-type
|
|
78
79
|
if (req.body !== undefined && req.body !== null) {
|
|
79
|
-
|
|
80
|
-
body = JSON.stringify(resolved);
|
|
81
|
-
if (!headers['content-type'] && !headers['Content-Type']) headers['content-type'] = 'application/json';
|
|
80
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
84
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
85
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
86
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
87
|
+
const ctx: APIRequestContext = await request.newContext({
|
|
88
|
+
baseURL: base,
|
|
89
|
+
extraHTTPHeaders: headers,
|
|
90
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
91
|
+
});
|
|
87
92
|
try {
|
|
88
|
-
res = await fetch(
|
|
93
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
let parsed: any = text;
|
|
96
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
97
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
89
98
|
} finally {
|
|
90
|
-
|
|
99
|
+
await ctx.dispose();
|
|
91
100
|
}
|
|
92
|
-
const text = await res.text();
|
|
93
|
-
let parsed: any = text;
|
|
94
|
-
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
95
|
-
const outHeaders: Record<string, string> = {};
|
|
96
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
97
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.1.2-beta.
|
|
3
|
+
"version": "3.1.2-beta.104",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=18.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@sungen/driver-ui": "3.1.2-beta.
|
|
36
|
+
"@sungen/driver-ui": "3.1.2-beta.104",
|
|
37
37
|
"@anthropic-ai/sdk": "^0.71.0",
|
|
38
38
|
"@babel/parser": "^7.28.5",
|
|
39
39
|
"@babel/traverse": "^7.28.5",
|
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
|
|
|
69
69
|
- VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
|
|
70
70
|
- VP-SEC = checks access control and malicious input
|
|
71
71
|
|
|
72
|
+
### Domain category codes — required for the coverage-balance gate
|
|
73
|
+
|
|
74
|
+
The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
|
|
75
|
+
|
|
76
|
+
| Bucket | Codes | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
|
|
79
|
+
| presentation | `UI` | layout / visual state |
|
|
80
|
+
| validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
|
|
81
|
+
| behavior | `LOGIC` | action-driven state changes |
|
|
82
|
+
| navigation | `NAV` | landing on / moving between pages |
|
|
83
|
+
|
|
84
|
+
**On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
|
|
85
|
+
|
|
72
86
|
---
|
|
73
87
|
|
|
74
88
|
## Shared Checks
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { request, type APIRequestContext } from '@playwright/test';
|
|
15
16
|
|
|
16
17
|
interface ApiDataSource {
|
|
17
18
|
kind?: string;
|
|
@@ -71,30 +72,32 @@ class ApiClient {
|
|
|
71
72
|
const { conf } = this.cfg(req.datasource);
|
|
72
73
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
73
74
|
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
74
|
-
const
|
|
75
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
75
76
|
|
|
76
|
-
let body: string | undefined;
|
|
77
77
|
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
78
|
+
let data: any; // JSON body — Playwright serializes + sets content-type
|
|
78
79
|
if (req.body !== undefined && req.body !== null) {
|
|
79
|
-
|
|
80
|
-
body = JSON.stringify(resolved);
|
|
81
|
-
if (!headers['content-type'] && !headers['Content-Type']) headers['content-type'] = 'application/json';
|
|
80
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
84
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
85
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
86
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
87
|
+
const ctx: APIRequestContext = await request.newContext({
|
|
88
|
+
baseURL: base,
|
|
89
|
+
extraHTTPHeaders: headers,
|
|
90
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
91
|
+
});
|
|
87
92
|
try {
|
|
88
|
-
res = await fetch(
|
|
93
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
let parsed: any = text;
|
|
96
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
97
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
89
98
|
} finally {
|
|
90
|
-
|
|
99
|
+
await ctx.dispose();
|
|
91
100
|
}
|
|
92
|
-
const text = await res.text();
|
|
93
|
-
let parsed: any = text;
|
|
94
|
-
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
95
|
-
const outHeaders: Record<string, string> = {};
|
|
96
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
97
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|