aios-management-web 0.1.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/.env.json +21 -0
- package/README.md +257 -0
- package/data/management-console.db +0 -0
- package/data/management-console.db-shm +0 -0
- package/data/management-console.db-wal +0 -0
- package/dist/assets/index-CV_wjCAG.js +464 -0
- package/dist/assets/index-DfMPB0eV.css +1 -0
- package/dist/index.html +13 -0
- package/docs/spec.md +199 -0
- package/index.html +12 -0
- package/package.json +37 -0
- package/scripts/reset-kernel.js +59 -0
- package/scripts/reset-password.js +22 -0
- package/server/fakes.js +57 -0
- package/server/index.js +21 -0
- package/server/src/api/middleware/auth.js +29 -0
- package/server/src/api/middleware/internal.js +44 -0
- package/server/src/api/routes/index.js +677 -0
- package/server/src/app.js +90 -0
- package/server/src/background/index.js +106 -0
- package/server/src/background/protocol.js +15 -0
- package/server/src/config/env.js +90 -0
- package/server/src/db/index.js +501 -0
- package/server/src/infra/mqtt/management-rpc-client.js +213 -0
- package/server/src/infra/providers/hzg-provider-client.js +39 -0
- package/server/src/infra/s3/object-storage.js +97 -0
- package/server/src/services/agent-quota.js +54 -0
- package/server/src/services/agent-service.js +696 -0
- package/server/src/services/agent-status-sync-service.js +132 -0
- package/server/src/services/audit-log-service.js +39 -0
- package/server/src/services/auth-service.js +153 -0
- package/server/src/services/catalog-sync-service.js +712 -0
- package/server/src/services/external-service.js +308 -0
- package/server/src/services/kernel-reset-service.js +86 -0
- package/server/src/services/portal-service.js +555 -0
- package/server/src/services/system-service.js +580 -0
- package/server/src/services/topic-ping-service.js +282 -0
- package/server/src/utils/errors.js +36 -0
- package/server/src/utils/security.js +22 -0
- package/server/test/agent-service-alignment.test.js +316 -0
- package/server/test/agent-service-create.test.js +662 -0
- package/server/test/agent-status-sync-service.test.js +167 -0
- package/server/test/agent-update-audit.test.js +63 -0
- package/server/test/auth-middleware.test.js +71 -0
- package/server/test/background-services.test.js +160 -0
- package/server/test/catalog-sync-service.test.js +920 -0
- package/server/test/db-reset-migration.test.js +123 -0
- package/server/test/env-config.test.js +68 -0
- package/server/test/external-service.test.js +380 -0
- package/server/test/hzg-provider-client.test.js +50 -0
- package/server/test/internal-auth-middleware.test.js +66 -0
- package/server/test/kernel-reset-service.test.js +112 -0
- package/server/test/management-rpc-client.test.js +105 -0
- package/server/test/portal-service-access-tokens.test.js +121 -0
- package/server/test/portal-service-alignment.test.js +318 -0
- package/server/test/portal-service-management-logs.test.js +114 -0
- package/server/test/reset-kernel-cli.test.js +23 -0
- package/server/test/service-api-auth-middleware.test.js +59 -0
- package/server/test/system-service-alignment.test.js +265 -0
- package/server/test/topic-ping-service.test.js +182 -0
- package/server/test/usage-refresh-audit-route.test.js +82 -0
- package/src/App.jsx +1 -0
- package/src/api.js +1 -0
- package/src/app/App.jsx +346 -0
- package/src/app/api-client.js +112 -0
- package/src/components/AppShell.jsx +117 -0
- package/src/components/CardTitleWithReload.jsx +20 -0
- package/src/components/DeleteActionButton.jsx +31 -0
- package/src/main.jsx +14 -0
- package/src/pages/AgentsPage.jsx +647 -0
- package/src/pages/AiosUsersPage.jsx +151 -0
- package/src/pages/DashboardPage.jsx +72 -0
- package/src/pages/LoginPage.jsx +41 -0
- package/src/pages/SettingsPage.jsx +431 -0
- package/src/pages/SkillsPage.jsx +175 -0
- package/src/pages/SystemLogsPage.jsx +349 -0
- package/src/pages/SystemsPage.jsx +498 -0
- package/src/pages/TemplatesPage.jsx +207 -0
- package/src/pages/UserManagementPage.jsx +25 -0
- package/src/pages/UsersPage.jsx +192 -0
- package/src/pages/system-logs/SystemLogsTabs.jsx +362 -0
- package/src/styles.css +222 -0
- package/src/utils/format.js +63 -0
- package/test/.reports/fast-2026-05-25T08-32-39-420Z.json +299 -0
- package/test/integration/common.js +208 -0
- package/test/integration/fast.js +135 -0
- package/test/integration/full.js +306 -0
- package/test/run-browser-e2e.js +212 -0
- package/test/run-jasmine.js +21 -0
- package/test/setup.js +1 -0
- package/vite.config.js +12 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "aios-web-portal-test-"));
|
|
6
|
+
process.env.AIOS_WEB_DATA_DIR = dataDir;
|
|
7
|
+
const { PortalService } = await import("../src/services/portal-service.js");
|
|
8
|
+
|
|
9
|
+
function createDb(rows) {
|
|
10
|
+
return {
|
|
11
|
+
prepare(sql) {
|
|
12
|
+
return {
|
|
13
|
+
get() {
|
|
14
|
+
if (sql.includes("COUNT(*) AS count FROM management_requests")) {
|
|
15
|
+
return { count: rows.length };
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
all(limit, offset) {
|
|
20
|
+
if (sql.includes("FROM management_requests")) {
|
|
21
|
+
return rows.slice(offset, offset + limit);
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
},
|
|
25
|
+
run() {
|
|
26
|
+
return { lastInsertRowid: 1 };
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it("lists kernel management requests with parsed payloads and response time", () => {
|
|
34
|
+
const service = new PortalService({
|
|
35
|
+
db: createDb([
|
|
36
|
+
{
|
|
37
|
+
id: 2,
|
|
38
|
+
request_id: "req-2",
|
|
39
|
+
action: "gateway.status",
|
|
40
|
+
params_json: "{\"verbose\":true}",
|
|
41
|
+
ok: 1,
|
|
42
|
+
result_json: "{\"status\":\"ok\"}",
|
|
43
|
+
error_json: null,
|
|
44
|
+
created_at: "2026-05-28T01:00:00.000Z",
|
|
45
|
+
completed_at: "2026-05-28T01:00:01.250Z"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 1,
|
|
49
|
+
request_id: "req-1",
|
|
50
|
+
action: "agent.list",
|
|
51
|
+
params_json: "{}",
|
|
52
|
+
ok: 0,
|
|
53
|
+
result_json: null,
|
|
54
|
+
error_json: "{\"message\":\"failed\"}",
|
|
55
|
+
created_at: "2026-05-28T00:00:00.000Z",
|
|
56
|
+
completed_at: "2026-05-28T00:00:00.400Z"
|
|
57
|
+
}
|
|
58
|
+
]),
|
|
59
|
+
objectStorage: {},
|
|
60
|
+
rpcClient: {},
|
|
61
|
+
authService: {}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = service.listManagementRequests({ page: 1, pageSize: 1 });
|
|
65
|
+
|
|
66
|
+
expect(result.total).toBe(2);
|
|
67
|
+
expect(result.page).toBe(1);
|
|
68
|
+
expect(result.pageSize).toBe(1);
|
|
69
|
+
expect(result.items).toEqual([
|
|
70
|
+
jasmine.objectContaining({
|
|
71
|
+
request_id: "req-2",
|
|
72
|
+
action: "gateway.status",
|
|
73
|
+
ok: true,
|
|
74
|
+
params: { verbose: true },
|
|
75
|
+
result: { status: "ok" },
|
|
76
|
+
error: null,
|
|
77
|
+
response_time_ms: 1250
|
|
78
|
+
})
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps pending requests unresolved and returns null duration when incomplete", () => {
|
|
83
|
+
const service = new PortalService({
|
|
84
|
+
db: createDb([
|
|
85
|
+
{
|
|
86
|
+
id: 3,
|
|
87
|
+
request_id: "req-pending",
|
|
88
|
+
action: "diagnostics.run",
|
|
89
|
+
params_json: "{\"deep\":true}",
|
|
90
|
+
ok: null,
|
|
91
|
+
result_json: null,
|
|
92
|
+
error_json: null,
|
|
93
|
+
created_at: "2026-05-28T02:00:00.000Z",
|
|
94
|
+
completed_at: null
|
|
95
|
+
}
|
|
96
|
+
]),
|
|
97
|
+
objectStorage: {},
|
|
98
|
+
rpcClient: {},
|
|
99
|
+
authService: {}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = service.listManagementRequests();
|
|
103
|
+
|
|
104
|
+
expect(result.items).toEqual([
|
|
105
|
+
jasmine.objectContaining({
|
|
106
|
+
request_id: "req-pending",
|
|
107
|
+
ok: null,
|
|
108
|
+
params: { deep: true },
|
|
109
|
+
result: null,
|
|
110
|
+
error: null,
|
|
111
|
+
response_time_ms: null
|
|
112
|
+
})
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseResetKernelArgs } from "../../scripts/reset-kernel.js";
|
|
2
|
+
|
|
3
|
+
it("parses dry-run and confirm flags for kernel reset cli", () => {
|
|
4
|
+
expect(parseResetKernelArgs([])).toEqual({
|
|
5
|
+
dryRun: false,
|
|
6
|
+
confirmed: false
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
expect(parseResetKernelArgs(["--dry-run"])).toEqual({
|
|
10
|
+
dryRun: true,
|
|
11
|
+
confirmed: false
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(parseResetKernelArgs(["--confirm-reset"])).toEqual({
|
|
15
|
+
dryRun: false,
|
|
16
|
+
confirmed: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(parseResetKernelArgs(["--confirm-reset", "--dry-run"])).toEqual({
|
|
20
|
+
dryRun: true,
|
|
21
|
+
confirmed: true
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createServiceApiAuthMiddleware } from "../src/api/middleware/internal.js";
|
|
2
|
+
|
|
3
|
+
function runMiddleware(middleware, { headers = {}, remoteAddress = "" } = {}) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
middleware({
|
|
6
|
+
headers,
|
|
7
|
+
socket: {
|
|
8
|
+
remoteAddress
|
|
9
|
+
}
|
|
10
|
+
}, {}, (error) => resolve(error ?? null));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
it("bypasses service api auth for localhost requests", async () => {
|
|
15
|
+
const middleware = createServiceApiAuthMiddleware({
|
|
16
|
+
hasAccessToken() {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const ipv4Error = await runMiddleware(middleware, {
|
|
22
|
+
remoteAddress: "127.0.0.1"
|
|
23
|
+
});
|
|
24
|
+
expect(ipv4Error).toBeNull();
|
|
25
|
+
|
|
26
|
+
const ipv6Error = await runMiddleware(middleware, {
|
|
27
|
+
remoteAddress: "::1"
|
|
28
|
+
});
|
|
29
|
+
expect(ipv6Error).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("requires bearer token for non-local service api requests", async () => {
|
|
33
|
+
const middleware = createServiceApiAuthMiddleware({
|
|
34
|
+
hasAccessToken(token) {
|
|
35
|
+
return token === "sk-abcdef";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const missingError = await runMiddleware(middleware, {
|
|
40
|
+
remoteAddress: "10.0.0.8"
|
|
41
|
+
});
|
|
42
|
+
expect(missingError.status).toBe(401);
|
|
43
|
+
|
|
44
|
+
const invalidError = await runMiddleware(middleware, {
|
|
45
|
+
remoteAddress: "10.0.0.8",
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: "Bearer sk-other"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
expect(invalidError.status).toBe(403);
|
|
51
|
+
|
|
52
|
+
const okError = await runMiddleware(middleware, {
|
|
53
|
+
remoteAddress: "10.0.0.8",
|
|
54
|
+
headers: {
|
|
55
|
+
authorization: "Bearer sk-abcdef"
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
expect(okError).toBeNull();
|
|
59
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "aios-web-system-test-"));
|
|
6
|
+
process.env.AIOS_WEB_DATA_DIR = dataDir;
|
|
7
|
+
const { SystemService } = await import("../src/services/system-service.js");
|
|
8
|
+
|
|
9
|
+
function createDb({ currentSystem = null, createdSystem = null } = {}) {
|
|
10
|
+
const statements = [];
|
|
11
|
+
return {
|
|
12
|
+
statements,
|
|
13
|
+
prepare(sql) {
|
|
14
|
+
return {
|
|
15
|
+
run: (...args) => {
|
|
16
|
+
statements.push({ sql, args });
|
|
17
|
+
return { lastInsertRowid: 1 };
|
|
18
|
+
},
|
|
19
|
+
get: (...args) => {
|
|
20
|
+
if (sql.includes("FROM artifacts")) {
|
|
21
|
+
return currentSystem?.ontology_artifact_id ? {
|
|
22
|
+
bucket: "admin-in",
|
|
23
|
+
object_key: "ontology/existing.zip"
|
|
24
|
+
} : null;
|
|
25
|
+
}
|
|
26
|
+
if (sql.includes("WHERE application_name = ?")) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (sql.includes("FROM business_systems s") || sql.includes("SELECT * FROM business_systems WHERE id = ?")) {
|
|
30
|
+
if (Number(args[0]) === 1 && createdSystem) {
|
|
31
|
+
return createdSystem;
|
|
32
|
+
}
|
|
33
|
+
return currentSystem;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
all: () => []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it("maps website fields to management-service apps.create params", async () => {
|
|
44
|
+
const createdSystem = {
|
|
45
|
+
id: 1,
|
|
46
|
+
provider: "hzg",
|
|
47
|
+
application_name: "demo-app",
|
|
48
|
+
description: "desc",
|
|
49
|
+
ontology_artifact_id: 9,
|
|
50
|
+
scheme: "https",
|
|
51
|
+
host: "example.com",
|
|
52
|
+
port: 443,
|
|
53
|
+
status: "active",
|
|
54
|
+
last_connectivity_test_status: "unknown",
|
|
55
|
+
last_connectivity_test_result_json: "{}",
|
|
56
|
+
updated_at: "2026-05-27T00:00:00.000Z"
|
|
57
|
+
};
|
|
58
|
+
const db = createDb({ createdSystem });
|
|
59
|
+
const rpcCalls = [];
|
|
60
|
+
const objectStorage = {
|
|
61
|
+
async uploadAdminArtifact() {
|
|
62
|
+
return {
|
|
63
|
+
bucket: "admin-in",
|
|
64
|
+
objectKey: "ontology/demo.zip"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const service = new SystemService({
|
|
69
|
+
db,
|
|
70
|
+
objectStorage,
|
|
71
|
+
rpcClient: {
|
|
72
|
+
async call(action, params) {
|
|
73
|
+
rpcCalls.push({ action, params });
|
|
74
|
+
if (action === "apps.list") {
|
|
75
|
+
return {
|
|
76
|
+
items: [{
|
|
77
|
+
id: "demo-app"
|
|
78
|
+
}]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const zipBuffer = Buffer.from("PK\u0005\u0006" + "\u0000".repeat(18), "binary");
|
|
87
|
+
|
|
88
|
+
await expectAsync(service.createSystem({
|
|
89
|
+
payload: {
|
|
90
|
+
provider: "hzg",
|
|
91
|
+
application_name: "demo-app"
|
|
92
|
+
},
|
|
93
|
+
file: null,
|
|
94
|
+
createdBy: 1
|
|
95
|
+
})).toBeRejectedWithError(/协议/);
|
|
96
|
+
|
|
97
|
+
service.persistOntologyArtifact = async () => ({
|
|
98
|
+
id: 9,
|
|
99
|
+
bucket: "admin-in",
|
|
100
|
+
objectKey: "ontology/demo.zip"
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await service.createSystem({
|
|
104
|
+
payload: {
|
|
105
|
+
provider: "hzg",
|
|
106
|
+
application_name: "demo-app",
|
|
107
|
+
description: "desc",
|
|
108
|
+
scheme: "https",
|
|
109
|
+
host: "example.com",
|
|
110
|
+
port: 443,
|
|
111
|
+
status: "active"
|
|
112
|
+
},
|
|
113
|
+
file: {
|
|
114
|
+
buffer: zipBuffer,
|
|
115
|
+
originalname: "demo.zip",
|
|
116
|
+
mimetype: "application/zip",
|
|
117
|
+
size: zipBuffer.length
|
|
118
|
+
},
|
|
119
|
+
createdBy: 1
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(result.system.application_name).toBe("demo-app");
|
|
123
|
+
expect(result.system.base_url).toBe("https://example.com:443/demo-app");
|
|
124
|
+
expect(result.kernelOntologyDeleted).toBe(true);
|
|
125
|
+
expect(result.deletedKernelOntologyNames).toEqual(["demo-app"]);
|
|
126
|
+
expect(rpcCalls.length).toBe(3);
|
|
127
|
+
expect(rpcCalls[0]).toEqual({
|
|
128
|
+
action: "apps.list",
|
|
129
|
+
params: {}
|
|
130
|
+
});
|
|
131
|
+
expect(rpcCalls[1]).toEqual({
|
|
132
|
+
action: "apps.delete",
|
|
133
|
+
params: {
|
|
134
|
+
systemId: "demo-app"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
expect(rpcCalls[2].action).toBe("apps.create");
|
|
138
|
+
expect(rpcCalls[2].params).toEqual({
|
|
139
|
+
systemId: "demo-app",
|
|
140
|
+
name: "demo-app",
|
|
141
|
+
provider: "hzg",
|
|
142
|
+
ontologyName: "demo-app",
|
|
143
|
+
description: "desc",
|
|
144
|
+
applicationEndpoint: "https://example.com:443/demo-app",
|
|
145
|
+
sessionServiceEndpoint: "https://example.com:443/demo-app",
|
|
146
|
+
status: "active",
|
|
147
|
+
bucket: "admin-in",
|
|
148
|
+
objectKey: "ontology/demo.zip",
|
|
149
|
+
replaceOntology: true
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("maps scheme/host/port fields to apps.update params", async () => {
|
|
154
|
+
const currentSystem = {
|
|
155
|
+
id: 1,
|
|
156
|
+
provider: "hzg",
|
|
157
|
+
application_name: "demo-app",
|
|
158
|
+
description: "old desc",
|
|
159
|
+
ontology_artifact_id: 7,
|
|
160
|
+
scheme: "https",
|
|
161
|
+
host: "old.example.com",
|
|
162
|
+
port: 443,
|
|
163
|
+
status: "disabled"
|
|
164
|
+
};
|
|
165
|
+
const updatedSystem = {
|
|
166
|
+
...currentSystem,
|
|
167
|
+
description: "new desc",
|
|
168
|
+
host: "new.example.com",
|
|
169
|
+
status: "active"
|
|
170
|
+
};
|
|
171
|
+
const db = createDb({ currentSystem, createdSystem: updatedSystem });
|
|
172
|
+
const rpcCalls = [];
|
|
173
|
+
const service = new SystemService({
|
|
174
|
+
db,
|
|
175
|
+
objectStorage: {},
|
|
176
|
+
rpcClient: {
|
|
177
|
+
async call(action, params) {
|
|
178
|
+
rpcCalls.push({ action, params });
|
|
179
|
+
if (action === "apps.list") {
|
|
180
|
+
return {
|
|
181
|
+
items: [{
|
|
182
|
+
id: "demo-app"
|
|
183
|
+
}]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await service.updateSystem(1, {
|
|
192
|
+
payload: {
|
|
193
|
+
description: "new desc",
|
|
194
|
+
host: "new.example.com",
|
|
195
|
+
status: "active"
|
|
196
|
+
},
|
|
197
|
+
file: null,
|
|
198
|
+
createdBy: 1
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.system.application_name).toBe("demo-app");
|
|
202
|
+
expect(result.kernelOntologyDeleted).toBe(true);
|
|
203
|
+
expect(result.deletedKernelOntologyNames).toEqual(["demo-app"]);
|
|
204
|
+
expect(rpcCalls.length).toBe(3);
|
|
205
|
+
expect(rpcCalls[0]).toEqual({
|
|
206
|
+
action: "apps.list",
|
|
207
|
+
params: {}
|
|
208
|
+
});
|
|
209
|
+
expect(rpcCalls[1]).toEqual({
|
|
210
|
+
action: "apps.delete",
|
|
211
|
+
params: {
|
|
212
|
+
systemId: "demo-app"
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
expect(rpcCalls[2].action).toBe("apps.create");
|
|
216
|
+
expect(rpcCalls[2].params).toEqual({
|
|
217
|
+
systemId: "demo-app",
|
|
218
|
+
name: "demo-app",
|
|
219
|
+
provider: "hzg",
|
|
220
|
+
ontologyName: "demo-app",
|
|
221
|
+
description: "new desc",
|
|
222
|
+
applicationEndpoint: "https://new.example.com:443/demo-app",
|
|
223
|
+
sessionServiceEndpoint: "https://new.example.com:443/demo-app",
|
|
224
|
+
status: "active",
|
|
225
|
+
bucket: "admin-in",
|
|
226
|
+
objectKey: "ontology/existing.zip",
|
|
227
|
+
replaceOntology: true
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("rejects deleting built-in ontology systems", async () => {
|
|
232
|
+
const currentSystem = {
|
|
233
|
+
id: 1,
|
|
234
|
+
provider: "hzg",
|
|
235
|
+
application_name: "demo-app",
|
|
236
|
+
description: "desc",
|
|
237
|
+
ontology_artifact_id: 7,
|
|
238
|
+
scheme: "https",
|
|
239
|
+
host: "example.com",
|
|
240
|
+
port: 443,
|
|
241
|
+
status: "active",
|
|
242
|
+
is_builtin: 1
|
|
243
|
+
};
|
|
244
|
+
const db = createDb({ currentSystem });
|
|
245
|
+
const rpcCalls = [];
|
|
246
|
+
const service = new SystemService({
|
|
247
|
+
db,
|
|
248
|
+
objectStorage: {},
|
|
249
|
+
rpcClient: {
|
|
250
|
+
async call(action, params) {
|
|
251
|
+
rpcCalls.push({ action, params });
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await service.deleteSystem(1);
|
|
259
|
+
fail("Expected deleteSystem to reject");
|
|
260
|
+
} catch (error) {
|
|
261
|
+
expect(error.status).toBe(409);
|
|
262
|
+
expect(error.code).toBe("conflict");
|
|
263
|
+
}
|
|
264
|
+
expect(rpcCalls).toEqual([]);
|
|
265
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
|
|
3
|
+
const { TopicPingService } = await import("../src/services/topic-ping-service.js");
|
|
4
|
+
|
|
5
|
+
class MockMqttClient extends EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.subscriptions = [];
|
|
9
|
+
this.publications = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
subscribe(topic, _options, callback) {
|
|
13
|
+
this.subscriptions.push(topic);
|
|
14
|
+
callback?.(null);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
publish(topic, payload, _options, callback) {
|
|
18
|
+
this.publications.push({ topic, payload: JSON.parse(payload) });
|
|
19
|
+
callback?.(null);
|
|
20
|
+
queueMicrotask(() => {
|
|
21
|
+
const prompt = this.publications[0].payload.text;
|
|
22
|
+
const token = (prompt.match(/PONG ([A-Z0-9]+)/i)?.[1] || "").toUpperCase();
|
|
23
|
+
this.emit("message", this.subscriptions[0], Buffer.from(JSON.stringify({
|
|
24
|
+
sessionId: this.publications[0].payload.sessionId,
|
|
25
|
+
kind: "final",
|
|
26
|
+
text: `PONG ${token}`
|
|
27
|
+
}), "utf8"));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
end(_force, _options, callback) {
|
|
32
|
+
callback?.();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("pings admin topics and returns response time", async () => {
|
|
37
|
+
const client = new MockMqttClient();
|
|
38
|
+
const service = new TopicPingService({
|
|
39
|
+
db: {
|
|
40
|
+
prepare() {
|
|
41
|
+
return {
|
|
42
|
+
get() {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
env: {
|
|
49
|
+
mqtt: {
|
|
50
|
+
brokerUrl: "mqtt://broker",
|
|
51
|
+
username: "",
|
|
52
|
+
password: "",
|
|
53
|
+
adminInboundTopic: "aios/admin/inbound",
|
|
54
|
+
adminOutboundTopic: "aios/admin/outbound",
|
|
55
|
+
agentInboundTopicTemplate: "aios/agent/{agentId}/inbound",
|
|
56
|
+
agentOutboundTopicTemplate: "aios/agent/{agentId}/outbound"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
mqttFactory: {
|
|
60
|
+
connect() {
|
|
61
|
+
queueMicrotask(() => client.emit("connect"));
|
|
62
|
+
return client;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await service.pingAdmin(1000);
|
|
68
|
+
|
|
69
|
+
expect(result.ok).toBe(true);
|
|
70
|
+
expect(result.inboundTopic).toBe("aios/admin/inbound");
|
|
71
|
+
expect(result.outboundTopic).toBe("aios/admin/outbound");
|
|
72
|
+
expect(typeof result.responseTimeMs).toBe("number");
|
|
73
|
+
expect(client.subscriptions).toEqual(["aios/admin/outbound"]);
|
|
74
|
+
expect(client.publications[0].topic).toBe("aios/admin/inbound");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("waits until agent ping succeeds", async () => {
|
|
78
|
+
const service = new TopicPingService({
|
|
79
|
+
db: {
|
|
80
|
+
prepare() {
|
|
81
|
+
return {
|
|
82
|
+
get() {
|
|
83
|
+
return {
|
|
84
|
+
slug: "agent-a",
|
|
85
|
+
remote_state_json: JSON.stringify({
|
|
86
|
+
inboundTopic: "aios/agent/agent-a/inbound",
|
|
87
|
+
outboundTopic: "aios/agent/agent-a/outbound"
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
env: {
|
|
95
|
+
mqtt: {
|
|
96
|
+
brokerUrl: "mqtt://broker",
|
|
97
|
+
agentInboundTopicTemplate: "aios/agent/{agentId}/inbound",
|
|
98
|
+
agentOutboundTopicTemplate: "aios/agent/{agentId}/outbound"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let attempt = 0;
|
|
104
|
+
service.pingAgent = async () => {
|
|
105
|
+
attempt += 1;
|
|
106
|
+
if (attempt < 3) {
|
|
107
|
+
throw new Error("not ready");
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, responseTimeMs: 12 };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = await service.waitForAgentReady("agent-a", {
|
|
113
|
+
timeoutMs: 1000,
|
|
114
|
+
intervalMs: 1,
|
|
115
|
+
pingTimeoutMs: 100
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.ok).toBe(true);
|
|
119
|
+
expect(result.responseTimeMs).toBe(12);
|
|
120
|
+
expect(result.waitedMs).toEqual(jasmine.any(Number));
|
|
121
|
+
expect(attempt).toBe(3);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("waits until agent runtime becomes active without pinging the model", async () => {
|
|
125
|
+
const listeners = new Map();
|
|
126
|
+
const service = new TopicPingService({
|
|
127
|
+
db: {
|
|
128
|
+
prepare() {
|
|
129
|
+
return {
|
|
130
|
+
get() {
|
|
131
|
+
return {
|
|
132
|
+
slug: "agent-a",
|
|
133
|
+
remote_state_json: JSON.stringify({
|
|
134
|
+
inboundTopic: "aios/agent/agent-a/inbound",
|
|
135
|
+
outboundTopic: "aios/agent/agent-a/outbound"
|
|
136
|
+
})
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
env: {
|
|
143
|
+
mqtt: {
|
|
144
|
+
brokerUrl: "mqtt://broker",
|
|
145
|
+
agentInboundTopicTemplate: "aios/agent/{agentId}/inbound",
|
|
146
|
+
agentOutboundTopicTemplate: "aios/agent/{agentId}/outbound"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
rpcClient: {
|
|
150
|
+
on(event, handler) {
|
|
151
|
+
listeners.set(event, handler);
|
|
152
|
+
},
|
|
153
|
+
off(event, handler) {
|
|
154
|
+
if (listeners.get(event) === handler) {
|
|
155
|
+
listeners.delete(event);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
listeners.get("outbound_message")?.({
|
|
163
|
+
requestId: "req-1",
|
|
164
|
+
ok: true,
|
|
165
|
+
request: {
|
|
166
|
+
action: "agent.create",
|
|
167
|
+
params: {
|
|
168
|
+
agentId: "agent-a"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}, 5);
|
|
173
|
+
|
|
174
|
+
const result = await service.waitForAgentRuntimeReady("agent-a", {
|
|
175
|
+
timeoutMs: 200
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result.ok).toBe(true);
|
|
179
|
+
expect(result.inboundTopic).toBe("aios/agent/agent-a/inbound");
|
|
180
|
+
expect(result.outboundTopic).toBe("aios/agent/agent-a/outbound");
|
|
181
|
+
expect(result.waitedMs).toEqual(jasmine.any(Number));
|
|
182
|
+
});
|