@abgov/nx-adsp 12.5.0 → 12.7.0
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 +5 -4
- package/src/generators/angular-app/angular-app.js +0 -4
- package/src/generators/angular-app/angular-app.js.map +1 -1
- package/src/generators/angular-app/files/AGENTS.md__tmpl__ +96 -0
- package/src/generators/express-service/express-service.js +24 -0
- package/src/generators/express-service/express-service.js.map +1 -1
- package/src/generators/express-service/files/AGENTS.md__tmpl__ +76 -0
- package/src/generators/react-app/files/AGENTS.md__tmpl__ +76 -0
- package/src/utils/agent.d.ts +34 -0
- package/src/utils/agent.js +141 -0
- package/src/utils/agent.js.map +1 -0
- package/src/utils/agent.spec.ts +118 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abgov/nx-adsp",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.7.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"description": "Government of Alberta - Nx plugin for ADSP apps.",
|
|
@@ -13,16 +13,17 @@
|
|
|
13
13
|
"@abgov/nx-oc": "^12.0.0",
|
|
14
14
|
"@nx-dotnet/core": "^3.0.0",
|
|
15
15
|
"@nx/angular": "^22.0.0",
|
|
16
|
-
"@nx/express": "^22.0.0",
|
|
17
16
|
"@nx/devkit": "^22.0.0",
|
|
18
|
-
"@nx/react": "^22.0.0",
|
|
19
17
|
"@nx/eslint": "^22.0.0",
|
|
18
|
+
"@nx/express": "^22.0.0",
|
|
19
|
+
"@nx/react": "^22.0.0",
|
|
20
20
|
"tslib": "^2.0.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"axios": "^1.16.0",
|
|
24
24
|
"enquirer": "^2.3.6",
|
|
25
|
-
"json-schema-to-typescript": "^13.0.1"
|
|
25
|
+
"json-schema-to-typescript": "^13.0.1",
|
|
26
|
+
"socket.io-client": "^4.8.3"
|
|
26
27
|
},
|
|
27
28
|
"generators": "./generators.json",
|
|
28
29
|
"scripts": {},
|
|
@@ -50,10 +50,6 @@ function addFiles(host, options) {
|
|
|
50
50
|
}
|
|
51
51
|
return addProxyConf;
|
|
52
52
|
}
|
|
53
|
-
function removeFiles(host, options) {
|
|
54
|
-
host.delete(`${options.projectRoot}/src/app/logo.svg`);
|
|
55
|
-
host.delete(`${options.projectRoot}/src/app/star.svg`);
|
|
56
|
-
}
|
|
57
53
|
function default_1(host, options) {
|
|
58
54
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
59
55
|
var _a, _b;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"angular-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/angular-app/angular-app.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"angular-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/angular-app/angular-app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# AGENTS.md — <%= projectName %>
|
|
2
|
+
|
|
3
|
+
Angular 19 frontend for the Alberta Digital Service Platform (ADSP).
|
|
4
|
+
Generated by `nx g @abgov/nx-adsp:angular-app`.
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
|
|
8
|
+
- **UI**: Angular 19 standalone + GoA design system (`@abgov/angular-components` — `Goab*` components)
|
|
9
|
+
- **Auth**: `keycloak-angular` with `keycloak-js`
|
|
10
|
+
- **Router**: Angular Router with `createAuthGuard` from `keycloak-angular`
|
|
11
|
+
|
|
12
|
+
## Key files
|
|
13
|
+
|
|
14
|
+
| File | Purpose |
|
|
15
|
+
|------|---------|
|
|
16
|
+
| `src/main.ts` | Entry — bootstraps `AppComponent` with `appConfig`, imports `zone.js` |
|
|
17
|
+
| `src/app/app.config.ts` | `provideKeycloak` (PKCE, silent SSO), `provideRouter`, `provideHttpClient` |
|
|
18
|
+
| `src/app/app.component.ts` | Shell — auth state, hero banner background workaround, public API call |
|
|
19
|
+
| `src/app/app.routes.ts` | Routes — `/protected` with `createAuthGuard` |
|
|
20
|
+
| `src/app/protected/protected.component.ts` | Protected route — shows authenticated user info |
|
|
21
|
+
| `src/environments/environment.ts` | Access URL, realm, client ID — pre-set from ADSP tenant |
|
|
22
|
+
|
|
23
|
+
## Auth pattern
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import Keycloak from 'keycloak-js';
|
|
27
|
+
import { inject } from '@angular/core';
|
|
28
|
+
|
|
29
|
+
private keycloak = inject(Keycloak); // provided by provideKeycloak in app.config.ts
|
|
30
|
+
|
|
31
|
+
this.keycloak.authenticated // boolean
|
|
32
|
+
this.keycloak.tokenParsed?.['name'] // user display name
|
|
33
|
+
this.keycloak.login()
|
|
34
|
+
this.keycloak.logout({ redirectUri: window.location.origin })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Route guard using `createAuthGuard` from `keycloak-angular`:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const authGuard = createAuthGuard(async (_route, _state, { authenticated, keycloak }) => {
|
|
41
|
+
if (authenticated) return true;
|
|
42
|
+
await keycloak.login({ redirectUri: window.location.href });
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## GoA design system
|
|
48
|
+
|
|
49
|
+
Components are imported from `@abgov/angular-components` with the `Goab` prefix
|
|
50
|
+
and added to each standalone component's `imports` array:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { GoabButton, GoabAppHeader } from '@abgov/angular-components';
|
|
54
|
+
|
|
55
|
+
@Component({ imports: [GoabButton, GoabAppHeader], ... })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Known limitation:** `GoabHeroBanner.backgroundUrl` does not bind reliably via
|
|
59
|
+
Angular's template binding due to an `@if(isReady)` timing issue inside the
|
|
60
|
+
component wrapper. Use `MutationObserver` to set the property directly after
|
|
61
|
+
the inner `<goa-hero-banner>` element appears — see `app.component.ts` for the
|
|
62
|
+
established pattern.
|
|
63
|
+
|
|
64
|
+
## Backend API calls (mean / proxy setup)
|
|
65
|
+
|
|
66
|
+
If `proxy.conf.json` exists at the project root, API calls are proxied to the
|
|
67
|
+
backend service in development. Use relative `/api/` paths — do not hardcode
|
|
68
|
+
the service URL:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// ✓ correct — works through proxy in dev, nginx in production
|
|
72
|
+
this.http.get('/api/v1/my-resource')
|
|
73
|
+
|
|
74
|
+
// ✗ wrong — bypasses proxy, won't work in production
|
|
75
|
+
this.http.get('http://localhost:3333/my-service/v1/my-resource')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`includeBearerTokenInterceptor` (configured in `app.config.ts`) automatically
|
|
79
|
+
attaches the Keycloak access token to all outgoing HTTP requests.
|
|
80
|
+
In production, `nginx.conf` contains the same proxy rule routing `/api/` to
|
|
81
|
+
the backend service hostname.
|
|
82
|
+
|
|
83
|
+
## Adding a new route
|
|
84
|
+
|
|
85
|
+
1. Create `src/app/my-feature/my-feature.component.ts` as a standalone component
|
|
86
|
+
2. Add the route to `src/app/app.routes.ts`
|
|
87
|
+
3. Add required `Goab*` components to the new component's `imports` array
|
|
88
|
+
|
|
89
|
+
## What NOT to change
|
|
90
|
+
|
|
91
|
+
- `app.config.ts` — `provideKeycloak` initialises keycloak-js once; do not
|
|
92
|
+
create a second Keycloak instance elsewhere
|
|
93
|
+
- `silent-check-sso.html` — served from `public/`; required for keycloak-js
|
|
94
|
+
silent SSO check on page load
|
|
95
|
+
- `environments/environment.ts` — access URL and realm are pre-configured for
|
|
96
|
+
the ADSP tenant; override at runtime via environment variables
|
|
@@ -6,6 +6,10 @@ const nx_oc_1 = require("@abgov/nx-oc");
|
|
|
6
6
|
const devkit_1 = require("@nx/devkit");
|
|
7
7
|
const eslint_1 = require("@nx/eslint");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
const agent_1 = require("../../utils/agent");
|
|
10
|
+
// Version of nx-adsp passed to the agent for template compatibility checks.
|
|
11
|
+
// Keep in sync with package.json version.
|
|
12
|
+
const PLUGIN_VERSION = '12.x';
|
|
9
13
|
function normalizeOptions(host, options) {
|
|
10
14
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
11
15
|
const projectName = (0, devkit_1.names)(options.name).fileName;
|
|
@@ -22,6 +26,7 @@ function addFiles(host, options) {
|
|
|
22
26
|
}
|
|
23
27
|
function default_1(host, options) {
|
|
24
28
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
var _a, _b, _c, _d;
|
|
25
30
|
const normalizedOptions = yield normalizeOptions(host, options);
|
|
26
31
|
const { applicationGenerator: initExpress } = yield Promise.resolve().then(() => require('@nx/express'));
|
|
27
32
|
yield initExpress(host, Object.assign(Object.assign({}, options), { skipFormat: true, skipPackageJson: false, linter: eslint_1.Linter.EsLint, unitTestRunner: 'jest', js: false, directory: `apps/${options.name}` }));
|
|
@@ -42,6 +47,25 @@ function default_1(host, options) {
|
|
|
42
47
|
});
|
|
43
48
|
addFiles(host, normalizedOptions);
|
|
44
49
|
yield (0, devkit_1.formatFiles)(host);
|
|
50
|
+
// Consult the nx-adsp-agent to augment the project with ADSP capabilities.
|
|
51
|
+
// The agent has access to template tools and a workspace; it generates new
|
|
52
|
+
// files and modifications to integration files (main.ts, environment.ts)
|
|
53
|
+
// which are applied directly to the Nx Tree.
|
|
54
|
+
// Falls back silently if agent-service is unreachable or no accessToken.
|
|
55
|
+
if (normalizedOptions.adsp) {
|
|
56
|
+
const mainTs = (_b = (_a = host.read(`${normalizedOptions.projectRoot}/src/main.ts`)) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '';
|
|
57
|
+
const environmentTs = (_d = (_c = host.read(`${normalizedOptions.projectRoot}/src/environment.ts`)) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '';
|
|
58
|
+
yield (0, agent_1.consultAgent)(normalizedOptions.adsp.directoryServiceUrl, options.accessToken, {
|
|
59
|
+
projectName: normalizedOptions.projectName,
|
|
60
|
+
projectType: 'express-service',
|
|
61
|
+
tenant: normalizedOptions.adsp.tenant,
|
|
62
|
+
pluginVersion: PLUGIN_VERSION,
|
|
63
|
+
existingFiles: {
|
|
64
|
+
'src/main.ts': mainTs,
|
|
65
|
+
'src/environment.ts': environmentTs,
|
|
66
|
+
},
|
|
67
|
+
}, host, normalizedOptions.projectRoot);
|
|
68
|
+
}
|
|
45
69
|
yield (0, nx_oc_1.deploymentGenerator)(host, Object.assign(Object.assign({}, normalizedOptions), { appType: 'node', project: normalizedOptions.projectName }));
|
|
46
70
|
return () => {
|
|
47
71
|
(0, devkit_1.installPackagesTask)(host);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express-service.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/express-service/express-service.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"express-service.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/express-service/express-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AGENTS.md — <%= projectName %>
|
|
2
|
+
|
|
3
|
+
Node/Express backend service for the Alberta Digital Service Platform (ADSP).
|
|
4
|
+
Generated by `nx g @abgov/nx-adsp:express-service`.
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
|
|
8
|
+
- **Runtime**: Node.js + Express
|
|
9
|
+
- **Auth**: passport.js with tenant strategy from `@abgov/adsp-service-sdk`
|
|
10
|
+
- **Config**: `envalid` with `.env` file (see `src/environment.ts`)
|
|
11
|
+
- **SDK**: `@abgov/adsp-service-sdk` — provides service registration, auth,
|
|
12
|
+
event publishing, configuration management, and service discovery
|
|
13
|
+
|
|
14
|
+
## Key files
|
|
15
|
+
|
|
16
|
+
| File | Purpose |
|
|
17
|
+
|------|---------|
|
|
18
|
+
| `src/main.ts` | App entry — SDK init, middleware, routes, server start |
|
|
19
|
+
| `src/environment.ts` | Validated env config with defaults pre-set from ADSP tenant |
|
|
20
|
+
|
|
21
|
+
## SDK capabilities
|
|
22
|
+
|
|
23
|
+
`initializeService()` returns `capabilities`:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const { logger, tenantStrategy, traceHandler, configurationHandler,
|
|
27
|
+
healthCheck, directory, tokenProvider, eventService } = capabilities;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To resolve another ADSP service URL:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const url = await directory.getServiceUrl(AdspId.parse('urn:ads:platform:file-service'));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
To call another service with an access token:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const token = await tokenProvider.getAccessToken();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Adding routes
|
|
43
|
+
|
|
44
|
+
Add route handlers in `main.ts` under the `/<%= projectName %>/v1` path prefix:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
app.get('/<%= projectName %>/v1/my-resource', (req, res) => {
|
|
48
|
+
const user = req.user; // null for anonymous, populated for authenticated
|
|
49
|
+
res.json({ ... });
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`passport.authenticate(['tenant', 'anonymous'])` is applied to the entire
|
|
54
|
+
`/<%= projectName %>/v1` prefix — check `req.user` inside handlers to
|
|
55
|
+
distinguish authenticated from anonymous access.
|
|
56
|
+
|
|
57
|
+
## Frontend proxy integration (mern / mean stacks)
|
|
58
|
+
|
|
59
|
+
When this service is paired with a React or Angular frontend via the `mern` or
|
|
60
|
+
`mean` generators, the frontend proxies `/api/` to this service:
|
|
61
|
+
|
|
62
|
+
- **Dev** (webpack / Vite): `/api/v1/my-resource` → `/<%= projectName %>/v1/my-resource` on `localhost:3333`
|
|
63
|
+
- **Production** (nginx): same rewrite via the nginx proxy block in the frontend app
|
|
64
|
+
|
|
65
|
+
All API routes in this service live under `/<%= projectName %>/v1/`. The proxy
|
|
66
|
+
rewrites the frontend's `/api/` prefix to `/<%= projectName %>/` before the
|
|
67
|
+
request reaches Express, so `/<%= projectName %>/v1/public` maps to the
|
|
68
|
+
frontend's `/api/v1/public`.
|
|
69
|
+
|
|
70
|
+
## What NOT to change
|
|
71
|
+
|
|
72
|
+
- `initializeService(...)` config — `CLIENT_ID` and `CLIENT_SECRET` are read
|
|
73
|
+
from environment; do not hardcode credentials
|
|
74
|
+
- The passport strategy setup — the tenant strategy handles JWT validation
|
|
75
|
+
- `configurationHandler` on the API path — provides configuration service
|
|
76
|
+
integration; keep it applied before route handlers
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AGENTS.md — <%= projectName %>
|
|
2
|
+
|
|
3
|
+
React frontend for the Alberta Digital Service Platform (ADSP).
|
|
4
|
+
Generated by `nx g @abgov/nx-adsp:react-app`.
|
|
5
|
+
|
|
6
|
+
## Stack
|
|
7
|
+
|
|
8
|
+
- **UI**: React 18 + GoA design system (`@abgov/react-components` — `Goab*` components)
|
|
9
|
+
- **Auth**: `keycloak-js` via Redux Toolkit slice (`src/app/user.slice.ts`)
|
|
10
|
+
- **State**: Redux Toolkit — store in `src/store.ts`
|
|
11
|
+
- **Router**: React Router v6
|
|
12
|
+
|
|
13
|
+
## Key files
|
|
14
|
+
|
|
15
|
+
| File | Purpose |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `src/main.tsx` | Entry — Redux Provider, BrowserRouter, dispatches `initializeUser()` after render |
|
|
18
|
+
| `src/app/user.slice.ts` | Keycloak auth — login, logout, token refresh, `getAccessToken()` |
|
|
19
|
+
| `src/app/start.slice.ts` | Example public/private API calls |
|
|
20
|
+
| `src/app/config.slice.ts` | Loads ADSP directory service endpoints on startup |
|
|
21
|
+
| `src/app/intake.slice.ts` | Application domain state — extend this for your feature |
|
|
22
|
+
| `src/store.ts` | Redux store — add new slices here |
|
|
23
|
+
| `src/environments/environment.ts` | Access URL, realm, client ID — pre-set from ADSP tenant |
|
|
24
|
+
|
|
25
|
+
## Auth pattern
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { getAccessToken, loginUser, logoutUser, userSelector } from './user.slice';
|
|
29
|
+
|
|
30
|
+
// In a component:
|
|
31
|
+
const { authenticated, name } = useSelector(userSelector);
|
|
32
|
+
dispatch(loginUser()); // redirects to Keycloak
|
|
33
|
+
dispatch(logoutUser()); // redirects to Keycloak logout
|
|
34
|
+
|
|
35
|
+
// In a thunk (for authenticated API calls):
|
|
36
|
+
const token = await getAccessToken(); // refreshes token if needed
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## GoA design system
|
|
40
|
+
|
|
41
|
+
Components are imported from `@abgov/react-components` with the `Goab` prefix:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { GoabButton, GoabAppHeader, GoabButtonGroup } from '@abgov/react-components';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Adding a new Redux slice
|
|
48
|
+
|
|
49
|
+
1. Create `src/app/my-feature.slice.ts` using `createSlice` / `createAsyncThunk`
|
|
50
|
+
2. Export the reducer and add it to `src/store.ts`
|
|
51
|
+
|
|
52
|
+
## Backend API calls (mern / proxy setup)
|
|
53
|
+
|
|
54
|
+
If `proxy.conf.json` exists at the project root, API calls are proxied to the
|
|
55
|
+
backend service in development. Use relative `/api/` paths — do not hardcode
|
|
56
|
+
the service URL:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// ✓ correct — works through proxy in dev, nginx in production
|
|
60
|
+
const response = await fetch('/api/v1/my-resource');
|
|
61
|
+
|
|
62
|
+
// ✗ wrong — bypasses proxy, won't work in production
|
|
63
|
+
const response = await fetch('http://localhost:3333/my-service/v1/my-resource');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In production, `nginx.conf` contains the same proxy rule routing `/api/` to
|
|
67
|
+
the backend service hostname.
|
|
68
|
+
|
|
69
|
+
## What NOT to change
|
|
70
|
+
|
|
71
|
+
- `user.slice.ts` — keycloak-js is initialised once per app lifecycle; do not
|
|
72
|
+
create a second Keycloak instance
|
|
73
|
+
- `environments/environment.ts` — access URL and realm are pre-configured for
|
|
74
|
+
the ADSP tenant; override at runtime via deployment config
|
|
75
|
+
- `silent-check-sso.html` — required for keycloak-js silent SSO; must be served
|
|
76
|
+
from the app root
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
export interface AgentCapability {
|
|
3
|
+
generator: string;
|
|
4
|
+
params: Record<string, unknown>;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CapabilitySpec {
|
|
8
|
+
capabilities: AgentCapability[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Result returned when the agent conversation completes.
|
|
12
|
+
* filesWritten is the number of files applied to the Nx Tree from the
|
|
13
|
+
* agent workspace (new files and modified integration files like main.ts).
|
|
14
|
+
*/
|
|
15
|
+
export interface AgentResult {
|
|
16
|
+
filesWritten: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Connect to the ADSP agent-service and conduct a multi-turn conversation
|
|
20
|
+
* with the nx-adsp-agent. The agent uses its workspace tools to write
|
|
21
|
+
* generated and modified files; this function retrieves the workspace state
|
|
22
|
+
* after the conversation and applies all files to the Nx Tree.
|
|
23
|
+
*
|
|
24
|
+
* Returns null if agent-service is unavailable — callers should skip the
|
|
25
|
+
* agent step gracefully in that case.
|
|
26
|
+
*/
|
|
27
|
+
export declare function consultAgent(directoryServiceUrl: string, accessToken: string, projectContext: {
|
|
28
|
+
projectName: string;
|
|
29
|
+
projectType: 'express-service' | 'react-app' | 'angular-app';
|
|
30
|
+
tenant: string;
|
|
31
|
+
pluginVersion: string;
|
|
32
|
+
/** Content of key integration files for the agent to read and potentially modify. */
|
|
33
|
+
existingFiles: Record<string, string>;
|
|
34
|
+
}, host: Tree, projectRoot: string): Promise<AgentResult | null>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.consultAgent = consultAgent;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const readline_1 = require("readline");
|
|
6
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
7
|
+
const nx_oc_1 = require("@abgov/nx-oc");
|
|
8
|
+
const AGENT_SERVICE_URN = 'urn:ads:platform:agent-service:v1';
|
|
9
|
+
const AGENT_ID = 'nx-adsp-agent';
|
|
10
|
+
/**
|
|
11
|
+
* Connect to the ADSP agent-service and conduct a multi-turn conversation
|
|
12
|
+
* with the nx-adsp-agent. The agent uses its workspace tools to write
|
|
13
|
+
* generated and modified files; this function retrieves the workspace state
|
|
14
|
+
* after the conversation and applies all files to the Nx Tree.
|
|
15
|
+
*
|
|
16
|
+
* Returns null if agent-service is unavailable — callers should skip the
|
|
17
|
+
* agent step gracefully in that case.
|
|
18
|
+
*/
|
|
19
|
+
function consultAgent(directoryServiceUrl, accessToken, projectContext, host, projectRoot) {
|
|
20
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
const agentServiceUrl = yield resolveAgentServiceUrl(directoryServiceUrl);
|
|
22
|
+
if (!agentServiceUrl) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const socket = (0, socket_io_client_1.io)(agentServiceUrl, {
|
|
27
|
+
auth: { token: accessToken },
|
|
28
|
+
timeout: 30000,
|
|
29
|
+
reconnection: false,
|
|
30
|
+
});
|
|
31
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
32
|
+
const threadId = crypto.randomUUID();
|
|
33
|
+
let buffer = '';
|
|
34
|
+
let conversationDone = false;
|
|
35
|
+
// If stdin closes while waiting for user input, apply whatever the agent
|
|
36
|
+
// wrote to the workspace and continue generation.
|
|
37
|
+
rl.on('close', () => {
|
|
38
|
+
if (!conversationDone) {
|
|
39
|
+
requestWorkspaceState();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const buildInitialMessage = () => {
|
|
43
|
+
const fileSection = Object.entries(projectContext.existingFiles)
|
|
44
|
+
.map(([path, content]) => `${path}:\n\`\`\`typescript\n${content}\n\`\`\``)
|
|
45
|
+
.join('\n\n');
|
|
46
|
+
return (`I am setting up a new ${projectContext.projectType} called "${projectContext.projectName}" ` +
|
|
47
|
+
`for ADSP tenant "${projectContext.tenant}" (nx-adsp plugin version ${projectContext.pluginVersion}).\n\n` +
|
|
48
|
+
`Existing project files:\n\n${fileSection}\n\n` +
|
|
49
|
+
`What ADSP capabilities would be useful to integrate into this service?`);
|
|
50
|
+
};
|
|
51
|
+
const sendMessage = (content) => {
|
|
52
|
+
socket.emit('message', {
|
|
53
|
+
agent: AGENT_ID,
|
|
54
|
+
threadId,
|
|
55
|
+
content,
|
|
56
|
+
rawChunks: true,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const requestWorkspaceState = () => {
|
|
60
|
+
socket.emit('workspace-read', { agent: AGENT_ID, threadId });
|
|
61
|
+
};
|
|
62
|
+
const promptUser = () => {
|
|
63
|
+
rl.question('\n> ', (input) => {
|
|
64
|
+
const trimmed = input.trim();
|
|
65
|
+
if (trimmed) {
|
|
66
|
+
sendMessage(trimmed);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Empty input — user is skipping; resolve without files.
|
|
70
|
+
cleanup(0);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const applyWorkspaceFiles = (files) => {
|
|
75
|
+
let count = 0;
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const fullPath = `${projectRoot}/${file.path}`;
|
|
78
|
+
host.write(fullPath, file.content);
|
|
79
|
+
count++;
|
|
80
|
+
}
|
|
81
|
+
return count;
|
|
82
|
+
};
|
|
83
|
+
const cleanup = (filesWritten) => {
|
|
84
|
+
conversationDone = true;
|
|
85
|
+
rl.close();
|
|
86
|
+
socket.disconnect();
|
|
87
|
+
resolve(filesWritten > 0 ? { filesWritten } : null);
|
|
88
|
+
};
|
|
89
|
+
socket.on('connect', () => {
|
|
90
|
+
sendMessage(buildInitialMessage());
|
|
91
|
+
});
|
|
92
|
+
socket.on('stream', ({ chunk, done }) => {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
if ((chunk === null || chunk === void 0 ? void 0 : chunk.type) === 'text-delta') {
|
|
95
|
+
const text = (_b = (_a = chunk.payload) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
96
|
+
buffer += text;
|
|
97
|
+
process.stdout.write(text);
|
|
98
|
+
}
|
|
99
|
+
if (done) {
|
|
100
|
+
if (buffer.length > 0 && !buffer.endsWith('\n')) {
|
|
101
|
+
process.stdout.write('\n');
|
|
102
|
+
}
|
|
103
|
+
buffer = '';
|
|
104
|
+
// Agent has finished its response — request workspace state to get
|
|
105
|
+
// any files it wrote, or ask user for follow-up if needed.
|
|
106
|
+
requestWorkspaceState();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
socket.on('workspace-state', ({ files }) => {
|
|
110
|
+
if ((files === null || files === void 0 ? void 0 : files.length) > 0) {
|
|
111
|
+
const written = applyWorkspaceFiles(files);
|
|
112
|
+
process.stdout.write(`\nApplied ${written} file(s) from agent workspace.\n`);
|
|
113
|
+
cleanup(written);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// No files written yet — agent may need more input.
|
|
117
|
+
promptUser();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
socket.on('session-expired', () => {
|
|
121
|
+
process.stdout.write('\nAgent session expired.\n');
|
|
122
|
+
requestWorkspaceState();
|
|
123
|
+
});
|
|
124
|
+
socket.on('connect_error', () => cleanup(0));
|
|
125
|
+
socket.on('error', () => requestWorkspaceState());
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function resolveAgentServiceUrl(directoryServiceUrl) {
|
|
130
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
131
|
+
var _a;
|
|
132
|
+
try {
|
|
133
|
+
const urls = yield (0, nx_oc_1.getServiceUrls)(directoryServiceUrl);
|
|
134
|
+
return (_a = urls[AGENT_SERVICE_URN]) !== null && _a !== void 0 ? _a : null;
|
|
135
|
+
}
|
|
136
|
+
catch (_b) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/agent.ts"],"names":[],"mappings":";;AAsCA,oCAuIC;;AA7KD,uCAA2C;AAE3C,uDAAsC;AACtC,wCAA8C;AAE9C,MAAM,iBAAiB,GAAG,mCAAmC,CAAC;AAC9D,MAAM,QAAQ,GAAG,eAAe,CAAC;AAuBjC;;;;;;;;GAQG;AACH,SAAsB,YAAY,CAChC,mBAA2B,EAC3B,WAAmB,EACnB,cAOC,EACD,IAAU,EACV,WAAmB;;QAEnB,MAAM,eAAe,GAAG,MAAM,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;QAC1E,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,MAAM,GAAG,IAAA,qBAAE,EAAC,eAAe,EAAE;gBACjC,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE;gBAC5B,OAAO,EAAE,KAAK;gBACd,YAAY,EAAE,KAAK;aACpB,CAAC,CAAC;YAEH,MAAM,EAAE,GAAG,IAAA,0BAAe,EAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7E,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;YACrC,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,gBAAgB,GAAG,KAAK,CAAC;YAE7B,yEAAyE;YACzE,kDAAkD;YAClD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBAClB,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACtB,qBAAqB,EAAE,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,mBAAmB,GAAG,GAAG,EAAE;gBAC/B,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC;qBAC7D,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,wBAAwB,OAAO,UAAU,CAAC;qBAC1E,IAAI,CAAC,MAAM,CAAC,CAAC;gBAEhB,OAAO,CACL,yBAAyB,cAAc,CAAC,WAAW,YAAY,cAAc,CAAC,WAAW,IAAI;oBAC7F,oBAAoB,cAAc,CAAC,MAAM,6BAA6B,cAAc,CAAC,aAAa,QAAQ;oBAC1G,8BAA8B,WAAW,MAAM;oBAC/C,wEAAwE,CACzE,CAAC;YACJ,CAAC,CAAC;YAEF,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,EAAE;gBACtC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE;oBACrB,KAAK,EAAE,QAAQ;oBACf,QAAQ;oBACR,OAAO;oBACP,SAAS,EAAE,IAAI;iBAChB,CAAC,CAAC;YACL,CAAC,CAAC;YAEF,MAAM,qBAAqB,GAAG,GAAG,EAAE;gBACjC,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/D,CAAC,CAAC;YAEF,MAAM,UAAU,GAAG,GAAG,EAAE;gBACtB,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;oBAC7B,IAAI,OAAO,EAAE,CAAC;wBACZ,WAAW,CAAC,OAAO,CAAC,CAAC;oBACvB,CAAC;yBAAM,CAAC;wBACN,yDAAyD;wBACzD,OAAO,CAAC,CAAC,CAAC,CAAC;oBACb,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YAEF,MAAM,mBAAmB,GAAG,CAAC,KAA0C,EAAE,EAAE;gBACzE,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,MAAM,QAAQ,GAAG,GAAG,WAAW,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;oBAC/C,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;oBACnC,KAAK,EAAE,CAAC;gBACV,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC,CAAC;YAEF,MAAM,OAAO,GAAG,CAAC,YAAoB,EAAE,EAAE;gBACvC,gBAAgB,GAAG,IAAI,CAAC;gBACxB,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,CAAC,UAAU,EAAE,CAAC;gBACpB,OAAO,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACtD,CAAC,CAAC;YAEF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;gBACxB,WAAW,CAAC,mBAAmB,EAAE,CAAC,CAAC;YACrC,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;;gBACtC,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,MAAK,YAAY,EAAE,CAAC;oBACjC,MAAM,IAAI,GAAW,MAAA,MAAA,KAAK,CAAC,OAAO,0CAAE,IAAI,mCAAI,EAAE,CAAC;oBAC/C,MAAM,IAAI,IAAI,CAAC;oBACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC7B,CAAC;gBAED,IAAI,IAAI,EAAE,CAAC;oBACT,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;wBAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC7B,CAAC;oBACD,MAAM,GAAG,EAAE,CAAC;oBACZ,mEAAmE;oBACnE,2DAA2D;oBAC3D,qBAAqB,EAAE,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAkD,EAAE,EAAE;gBACzF,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,MAAM,IAAG,CAAC,EAAE,CAAC;oBACtB,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;oBAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,OAAO,kCAAkC,CAAC,CAAC;oBAC7E,OAAO,CAAC,OAAO,CAAC,CAAC;gBACnB,CAAC;qBAAM,CAAC;oBACN,oDAAoD;oBACpD,UAAU,EAAE,CAAC;gBACf,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;gBAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;gBACnD,qBAAqB,EAAE,CAAC;YAC1B,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC;CAAA;AAED,SAAe,sBAAsB,CACnC,mBAA2B;;;QAE3B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAA,sBAAc,EAAC,mBAAmB,CAAC,CAAC;YACvD,OAAO,MAAA,IAAI,CAAC,iBAAiB,CAAC,mCAAI,IAAI,CAAC;QACzC,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CAAA"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { consultAgent } from './agent';
|
|
2
|
+
|
|
3
|
+
jest.mock('readline', () => ({
|
|
4
|
+
createInterface: jest.fn(() => ({
|
|
5
|
+
question: jest.fn((_prompt: string, cb: (answer: string) => void) => cb('')),
|
|
6
|
+
close: jest.fn(),
|
|
7
|
+
on: jest.fn(),
|
|
8
|
+
})),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock('socket.io-client');
|
|
12
|
+
jest.mock('@abgov/nx-oc', () => ({ getServiceUrls: jest.fn() }));
|
|
13
|
+
jest.mock('@nx/devkit', () => ({ Tree: jest.fn() }));
|
|
14
|
+
|
|
15
|
+
import { io } from 'socket.io-client';
|
|
16
|
+
import { getServiceUrls } from '@abgov/nx-oc';
|
|
17
|
+
|
|
18
|
+
const mockedIo = jest.mocked(io);
|
|
19
|
+
const mockedGetServiceUrls = jest.mocked(getServiceUrls);
|
|
20
|
+
|
|
21
|
+
const PROJECT_CONTEXT = {
|
|
22
|
+
projectName: 'test-service',
|
|
23
|
+
projectType: 'express-service' as const,
|
|
24
|
+
tenant: 'test-tenant',
|
|
25
|
+
pluginVersion: '12.x',
|
|
26
|
+
existingFiles: {
|
|
27
|
+
'src/main.ts': 'const app = express();',
|
|
28
|
+
'src/environment.ts': 'export const environment = {};',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mockHost = { write: jest.fn(), read: jest.fn() } as unknown as import('@nx/devkit').Tree;
|
|
33
|
+
|
|
34
|
+
function makeMockSocket() {
|
|
35
|
+
const handlers: Record<string, (...args: unknown[]) => void> = {};
|
|
36
|
+
const socket = {
|
|
37
|
+
on: jest.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
38
|
+
handlers[event] = handler;
|
|
39
|
+
}),
|
|
40
|
+
emit: jest.fn(),
|
|
41
|
+
disconnect: jest.fn(),
|
|
42
|
+
_handlers: handlers,
|
|
43
|
+
};
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
mockedIo.mockReturnValue(socket as any);
|
|
46
|
+
return socket;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
50
|
+
|
|
51
|
+
describe('consultAgent', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.clearAllMocks();
|
|
54
|
+
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns null when agent-service is not in directory', async () => {
|
|
58
|
+
mockedGetServiceUrls.mockResolvedValue({});
|
|
59
|
+
const result = await consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns null on socket connect error', async () => {
|
|
64
|
+
mockedGetServiceUrls.mockResolvedValue({
|
|
65
|
+
'urn:ads:platform:agent-service:v1': 'https://agent.example.com',
|
|
66
|
+
});
|
|
67
|
+
const socket = makeMockSocket();
|
|
68
|
+
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
69
|
+
await flushPromises();
|
|
70
|
+
socket._handlers['connect_error']?.();
|
|
71
|
+
expect(await resultPromise).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('applies workspace files to Nx Tree and returns filesWritten count', async () => {
|
|
75
|
+
mockedGetServiceUrls.mockResolvedValue({
|
|
76
|
+
'urn:ads:platform:agent-service:v1': 'https://agent.example.com',
|
|
77
|
+
});
|
|
78
|
+
const socket = makeMockSocket();
|
|
79
|
+
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
80
|
+
await flushPromises();
|
|
81
|
+
|
|
82
|
+
socket._handlers['connect']?.();
|
|
83
|
+
socket._handlers['stream']?.({ chunk: null, done: true });
|
|
84
|
+
socket._handlers['workspace-state']?.({
|
|
85
|
+
files: [
|
|
86
|
+
{ path: 'src/roles.ts', content: 'export enum ServiceRoles {}' },
|
|
87
|
+
{ path: 'src/main.ts', content: 'updated main.ts' },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await resultPromise;
|
|
92
|
+
expect(result).toEqual({ filesWritten: 2 });
|
|
93
|
+
expect(mockHost.write).toHaveBeenCalledWith('apps/test-service/src/roles.ts', 'export enum ServiceRoles {}');
|
|
94
|
+
expect(mockHost.write).toHaveBeenCalledWith('apps/test-service/src/main.ts', 'updated main.ts');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('includes existing file content in the initial message', async () => {
|
|
98
|
+
mockedGetServiceUrls.mockResolvedValue({
|
|
99
|
+
'urn:ads:platform:agent-service:v1': 'https://agent.example.com',
|
|
100
|
+
});
|
|
101
|
+
const socket = makeMockSocket();
|
|
102
|
+
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
103
|
+
await flushPromises();
|
|
104
|
+
|
|
105
|
+
socket._handlers['connect']?.();
|
|
106
|
+
socket._handlers['stream']?.({ chunk: null, done: true });
|
|
107
|
+
socket._handlers['workspace-state']?.({ files: [] });
|
|
108
|
+
await resultPromise;
|
|
109
|
+
|
|
110
|
+
expect(socket.emit).toHaveBeenCalledWith(
|
|
111
|
+
'message',
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
agent: 'nx-adsp-agent',
|
|
114
|
+
content: expect.stringContaining('src/main.ts'),
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|