agentlas 0.7.0 → 0.9.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +205 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -1,322 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const assert = require("node:assert/strict");
5
- const fs = require("node:fs");
6
- const os = require("node:os");
7
- const path = require("node:path");
8
-
9
- const {
10
- runApi,
11
- DEFAULT_API_MODEL,
12
- ANTHROPIC_COMPAT_API,
13
- } = require("../engine/agentlas.cjs");
14
- const { create: createParity } = require("../engine/agentlas-parity.cjs");
15
-
16
- function response(status, payload, errorText) {
17
- return {
18
- ok: status >= 200 && status < 300,
19
- status,
20
- json: async () => payload,
21
- text: async () => errorText || "",
22
- };
23
- }
24
-
25
- function openFixtureDb(file) {
26
- try {
27
- const Database = require("better-sqlite3");
28
- return new Database(file);
29
- } catch {
30
- const { DatabaseSync } = require("node:sqlite");
31
- return new DatabaseSync(file);
32
- }
33
- }
34
-
35
- function quietUi() {
36
- const same = (value) => String(value ?? "");
37
- return {
38
- lang: "en",
39
- c: { paw: same, bold: same, text: same, dim: same },
40
- line() {},
41
- info() {},
42
- warn() {},
43
- error() {},
44
- tool() {},
45
- toolResult() {},
46
- startSpinner() {},
47
- stopSpinner() {},
48
- markdown() {},
49
- };
50
- }
51
-
52
- async function testAnthropicCompatibleProviders() {
53
- for (const backend of ["glm", "kimi", "deepseek"]) {
54
- const calls = [];
55
- const text = await runApi(backend, null, "system", "prompt", {
56
- apiKey: `${backend}-secret`,
57
- fetch: async (url, init) => {
58
- calls.push({ url, init });
59
- return response(200, { content: [{ type: "text", text: `${backend}-ok` }] });
60
- },
61
- });
62
-
63
- assert.equal(text, `${backend}-ok`);
64
- assert.equal(calls.length, 1);
65
- assert.equal(calls[0].url, `${ANTHROPIC_COMPAT_API[backend].baseUrl}/v1/messages`);
66
- assert.equal(calls[0].init.headers["x-api-key"], `${backend}-secret`);
67
- assert.equal(calls[0].init.headers.authorization, `Bearer ${backend}-secret`);
68
- assert.equal(calls[0].init.headers["anthropic-version"], "2023-06-01");
69
- assert.equal(JSON.parse(calls[0].init.body).model, DEFAULT_API_MODEL[backend]);
70
- }
71
- }
72
-
73
- async function testCustomBaseUrlComesFromSharedDb() {
74
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-run-api-"));
75
- const previous = process.env.AGENTLAS_USER_DATA_DIR;
76
- process.env.AGENTLAS_USER_DATA_DIR = dir;
77
- const db = openFixtureDb(path.join(dir, "agentlas.sqlite"));
78
- try {
79
- db.exec("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
80
- db.prepare("INSERT INTO meta(key, value) VALUES (?, ?)").run(
81
- "custom_base_url",
82
- "https://gateway.example.test/openai/v1/",
83
- );
84
-
85
- const calls = [];
86
- const text = await runApi("custom", "company-model", "system", "prompt", {
87
- apiKey: "custom-secret",
88
- fetch: async (url, init) => {
89
- calls.push({ url, init });
90
- return response(200, { choices: [{ message: { content: "custom-ok" } }] });
91
- },
92
- });
93
-
94
- assert.equal(text, "custom-ok");
95
- assert.equal(calls.length, 1);
96
- assert.equal(calls[0].url, "https://gateway.example.test/openai/v1/chat/completions");
97
- assert.equal(calls[0].init.headers.authorization, "Bearer custom-secret");
98
- assert.equal(JSON.parse(calls[0].init.body).model, "company-model");
99
-
100
- db.prepare("UPDATE meta SET value = ? WHERE key = ?").run(
101
- "http://public.example.test/v1",
102
- "custom_base_url",
103
- );
104
- let unsafeFetchCalled = false;
105
- await assert.rejects(
106
- runApi("custom", "company-model", "system", "prompt", {
107
- apiKey: "custom-secret",
108
- fetch: async () => {
109
- unsafeFetchCalled = true;
110
- return response(200, {});
111
- },
112
- }),
113
- /HTTPS.*localhost\/LAN/,
114
- );
115
- assert.equal(unsafeFetchCalled, false, "invalid public HTTP base URL must not receive the API key");
116
- } finally {
117
- try { db.close(); } catch { /* ignore */ }
118
- if (previous === undefined) delete process.env.AGENTLAS_USER_DATA_DIR;
119
- else process.env.AGENTLAS_USER_DATA_DIR = previous;
120
- fs.rmSync(dir, { recursive: true, force: true });
121
- }
122
- }
123
-
124
- async function testProviderErrorsNeverExitTheHostProcess() {
125
- const originalExit = process.exit;
126
- let exitCalls = 0;
127
- process.exit = (code) => {
128
- exitCalls += 1;
129
- throw new Error(`unexpected process.exit(${code})`);
130
- };
131
- try {
132
- await assert.rejects(
133
- runApi("openai", "gpt-test", "system", "prompt", {
134
- apiKey: "openai-secret",
135
- fetch: async () => response(429, null, "rate limited"),
136
- }),
137
- /OpenAI 429: rate limited/,
138
- );
139
- await assert.rejects(
140
- runApi("deepseek", "deepseek-chat", "system", "prompt", {
141
- apiKey: "",
142
- fetch: async () => response(200, {}),
143
- }),
144
- /deepseek API \ud0a4/,
145
- );
146
- await assert.rejects(
147
- runApi("unknown", null, "system", "prompt", {
148
- apiKey: "secret",
149
- fetch: async () => response(200, {}),
150
- }),
151
- /\uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 backend/,
152
- );
153
- assert.equal(exitCalls, 0, "runApi must throw to swarm/automation instead of exiting");
154
- } finally {
155
- process.exit = originalExit;
156
- }
157
- }
158
-
159
- async function testSwarmAndAutomationContainProviderFailure() {
160
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-run-api-host-"));
161
- const db = openFixtureDb(path.join(dir, "agentlas.sqlite"));
162
- const originalExit = process.exit;
163
- const originalExitCode = process.exitCode;
164
- let exitCalls = 0;
165
- process.exit = (code) => {
166
- exitCalls += 1;
167
- throw new Error(`unexpected process.exit(${code})`);
168
- };
169
- try {
170
- db.exec(`
171
- CREATE TABLE installed_agents (
172
- id TEXT PRIMARY KEY,
173
- name TEXT NOT NULL,
174
- system_prompt TEXT
175
- );
176
- CREATE TABLE automations (
177
- id TEXT PRIMARY KEY,
178
- name TEXT NOT NULL,
179
- schedule TEXT,
180
- target_type TEXT NOT NULL,
181
- target_id TEXT NOT NULL,
182
- prompt_template TEXT NOT NULL,
183
- enabled INTEGER NOT NULL,
184
- claimed_at TEXT,
185
- lease_owner TEXT,
186
- last_run_at TEXT,
187
- next_run_at TEXT,
188
- schedule_json TEXT,
189
- timezone TEXT,
190
- trigger_type TEXT,
191
- run_count INTEGER NOT NULL DEFAULT 0,
192
- max_runs INTEGER
193
- );
194
- CREATE TABLE run_history (
195
- id TEXT PRIMARY KEY,
196
- automation_id TEXT,
197
- scheduled_for TEXT,
198
- ran_at TEXT,
199
- status TEXT,
200
- skipped_count INTEGER,
201
- error TEXT
202
- );
203
- INSERT INTO installed_agents(id, name, system_prompt)
204
- VALUES ('agent-1', 'Regression Agent', 'System');
205
- INSERT INTO automations(
206
- id, name, schedule, target_type, target_id, prompt_template, enabled,
207
- next_run_at, timezone, trigger_type, run_count
208
- ) VALUES (
209
- 'automation-1', 'Regression Automation', 'daily-09:00', 'agent', 'agent-1', 'Run', 1,
210
- '2026-01-01T00:00:00.000Z', 'Asia/Seoul', 'schedule', 0
211
- );
212
- `);
213
-
214
- const failingRunApi = (backend, model, system, prompt) => runApi(backend, model, system, prompt, {
215
- apiKey: "openai-secret",
216
- fetch: async () => response(429, null, "rate limited"),
217
- });
218
- const parity = createParity({
219
- prefsLang: () => "en",
220
- resolveRuntime: () => ({ mode: "api", backend: "openai", model: "gpt-test" }),
221
- buildChildEnvCli: async () => ({}),
222
- runApi: failingRunApi,
223
- captureRuntime: async () => "",
224
- runCwd: () => dir,
225
- out() {},
226
- fail(message) { throw new Error(message); },
227
- });
228
-
229
- const swarm = await parity.swarmRun(db, "provider failure containment", { ui: quietUi(), concurrency: 2 });
230
- assert.equal(swarm.ok, false, "all-failed swarm should finish normally with ok=false");
231
-
232
- await parity.cmdAutomation(db, ["run", "automation-1"]);
233
- const automation = db.prepare(
234
- "SELECT claimed_at, lease_owner, last_run_at, run_count FROM automations WHERE id = ?",
235
- ).get("automation-1");
236
- assert.equal(automation.claimed_at, null, "automation lease must be released after provider error");
237
- assert.equal(automation.lease_owner, null, "automation lease owner must be cleared after provider error");
238
- assert.ok(automation.last_run_at, "automation failure must still be recorded");
239
- assert.equal(automation.run_count, 0, "failed automation must not count as success");
240
- const history = db.prepare(
241
- "SELECT status, error FROM run_history WHERE automation_id = ? ORDER BY ran_at DESC LIMIT 1",
242
- ).get("automation-1");
243
- assert.equal(history.status, "error");
244
- assert.match(history.error, /OpenAI 429: rate limited/);
245
- assert.equal(exitCalls, 0, "swarm/automation must contain provider errors without exiting the host");
246
-
247
- const fixedFrom = new Date("2026-07-10T00:30:00.000Z"); // 09:30 Asia/Seoul
248
- assert.equal(
249
- parity.nextAutomationRun(
250
- { schedule: "daily-09:00", schedule_json: null, timezone: "Asia/Seoul" },
251
- fixedFrom,
252
- ).toISOString(),
253
- "2026-07-11T00:00:00.000Z",
254
- "legacy desktop token must advance in its stored timezone",
255
- );
256
-
257
- const scheduledFailureRow = db.prepare("SELECT * FROM automations WHERE id = ?").get("automation-1");
258
- const failedScheduled = await parity.runAutomationOnce(db, scheduledFailureRow, {
259
- ui: quietUi(),
260
- advanceSchedule: true,
261
- scheduledFor: scheduledFailureRow.next_run_at,
262
- });
263
- assert.equal(failedScheduled.ok, false);
264
- const afterScheduledFailure = db.prepare(
265
- "SELECT next_run_at, run_count, enabled FROM automations WHERE id = ?",
266
- ).get("automation-1");
267
- assert.ok(
268
- Date.parse(afterScheduledFailure.next_run_at) > Date.now(),
269
- "failed scheduled automation must advance beyond now instead of retrying every poll",
270
- );
271
- assert.equal(afterScheduledFailure.run_count, 0, "failed run remains excluded from success count");
272
-
273
- db.prepare(
274
- `INSERT INTO automations(
275
- id, name, schedule, target_type, target_id, prompt_template, enabled,
276
- next_run_at, timezone, trigger_type, run_count
277
- ) VALUES ('automation-2', 'Success Automation', 'cron:*/5 * * * *', 'agent', 'agent-1', 'Run', 1,
278
- '2026-01-01T00:00:00.000Z', 'UTC', 'schedule', 0)`,
279
- ).run();
280
- const successParity = createParity({
281
- prefsLang: () => "en",
282
- resolveRuntime: () => ({ mode: "api", backend: "openai", model: "gpt-test" }),
283
- buildChildEnvCli: async () => ({}),
284
- runApi: async () => "ok",
285
- captureRuntime: async () => "",
286
- runCwd: () => dir,
287
- out() {},
288
- fail(message) { throw new Error(message); },
289
- });
290
- const successRow = db.prepare("SELECT * FROM automations WHERE id = ?").get("automation-2");
291
- const scheduledSuccess = await successParity.runAutomationOnce(db, successRow, {
292
- ui: quietUi(),
293
- advanceSchedule: true,
294
- scheduledFor: successRow.next_run_at,
295
- });
296
- assert.equal(scheduledSuccess.ok, true);
297
- const afterScheduledSuccess = db.prepare(
298
- "SELECT next_run_at, run_count, enabled FROM automations WHERE id = ?",
299
- ).get("automation-2");
300
- assert.ok(Date.parse(afterScheduledSuccess.next_run_at) > Date.now());
301
- assert.equal(afterScheduledSuccess.run_count, 1);
302
- assert.equal(afterScheduledSuccess.enabled, 1);
303
- } finally {
304
- process.exit = originalExit;
305
- process.exitCode = originalExitCode;
306
- try { db.close(); } catch { /* ignore */ }
307
- fs.rmSync(dir, { recursive: true, force: true });
308
- }
309
- }
310
-
311
- async function main() {
312
- await testAnthropicCompatibleProviders();
313
- await testCustomBaseUrlComesFromSharedDb();
314
- await testProviderErrorsNeverExitTheHostProcess();
315
- await testSwarmAndAutomationContainProviderFailure();
316
- process.stdout.write("run-api regression: PASS (BYOK parity + swarm/automation failure containment)\n");
317
- }
318
-
319
- main().catch((error) => {
320
- process.stderr.write(`${error && error.stack ? error.stack : error}\n`);
321
- process.exitCode = 1;
322
- });
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env node
2
- const assert = require("node:assert/strict");
3
- const runtime = require("../engine/agentlas.cjs");
4
-
5
- const base = {
6
- HOME: "/trusted/home",
7
- PATH: "/trusted/bin",
8
- CODEX_HOME: "/trusted/codex",
9
- AGENTLAS_CODEX_HOME: "/trusted/agentlas-codex",
10
- AGENTLAS_USER_DATA_DIR: "/trusted/agentlas-data",
11
- CLAUDE_CONFIG_DIR: "/trusted/claude",
12
- GEMINI_CLI_HOME: "/trusted/gemini",
13
- API_TOKEN: "old",
14
- };
15
- const maliciousDotenv = runtime.parseDotEnvCli([
16
- "HOME=/tmp/attacker",
17
- "PATH=/tmp/attacker/bin",
18
- "CODEX_HOME=/tmp/attacker/codex",
19
- "AGENTLAS_CODEX_HOME=/tmp/attacker/victim",
20
- "AGENTLAS_USER_DATA_DIR=/tmp/attacker/data",
21
- "CLAUDE_CONFIG_DIR=/tmp/attacker/claude",
22
- "GEMINI_CLI_HOME=/tmp/attacker/gemini",
23
- "GEMINI_CLI_EXTENSION_REGISTRY_URI=https://attacker.invalid/extensions",
24
- "CLAUDE_CODE_SAFE_MODE=1",
25
- "NODE_OPTIONS=--require=/tmp/attacker.js",
26
- "API_TOKEN=new",
27
- ].join("\n"));
28
- maliciousDotenv.Path = "/tmp/attacker/windows-bin";
29
-
30
- runtime.mergeChildEnvValuesCli(base, maliciousDotenv, true);
31
-
32
- assert.equal(base.HOME, "/trusted/home");
33
- assert.equal(base.PATH, "/trusted/bin");
34
- assert.equal(base.Path, undefined);
35
- assert.equal(base.CODEX_HOME, "/trusted/codex");
36
- assert.equal(base.AGENTLAS_CODEX_HOME, "/trusted/agentlas-codex");
37
- assert.equal(base.AGENTLAS_USER_DATA_DIR, "/trusted/agentlas-data");
38
- assert.equal(base.CLAUDE_CONFIG_DIR, "/trusted/claude");
39
- assert.equal(base.GEMINI_CLI_HOME, "/trusted/gemini");
40
- assert.equal(base.GEMINI_CLI_EXTENSION_REGISTRY_URI, undefined);
41
- assert.equal(base.CLAUDE_CODE_SAFE_MODE, undefined);
42
- assert.equal(base.NODE_OPTIONS, undefined);
43
- assert.equal(base.API_TOKEN, "new");
44
-
45
- // ── 네트워크 무결성 키: 비신뢰(프로젝트/에이전트) dotenv는 TLS/프록시/엔드포인트/세션을 못 바꾼다 ──
46
- // (bug-hunter 2026-07-12: 원샷 API 경로가 프로젝트 .env를 process.env에 병합해 MITM/SSRF/세션 하이재킹 가능했음)
47
- {
48
- const untrustedBase = { API_TOKEN: "keep" };
49
- const untrustedDotenv = runtime.parseDotEnvCli([
50
- "NODE_TLS_REJECT_UNAUTHORIZED=0",
51
- "HTTPS_PROXY=http://attacker.invalid:8080",
52
- "HTTP_PROXY=http://attacker.invalid:8080",
53
- "NODE_EXTRA_CA_CERTS=/tmp/attacker-ca.pem",
54
- "AGENTLAS_SESSION=forged-session",
55
- "AGENTLAS_MCP_BASE_URL=https://evil.example/mcp",
56
- "AGENTLAS_WEB_BASE_URL=https://evil.example",
57
- "OLLAMA_HOST=http://169.254.169.254",
58
- "OPENAI_API_KEY=sk-project-supplied", // 일반 API 키는 프로젝트가 넣을 수 있어야 함
59
- ].join("\n"));
60
- runtime.mergeChildEnvValuesCli(untrustedBase, untrustedDotenv, true /* overwrite */, false /* untrusted */);
61
- for (const k of ["NODE_TLS_REJECT_UNAUTHORIZED", "HTTPS_PROXY", "HTTP_PROXY", "NODE_EXTRA_CA_CERTS",
62
- "AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "OLLAMA_HOST"]) {
63
- assert.equal(untrustedBase[k], undefined, `untrusted dotenv must not set ${k}`);
64
- }
65
- assert.equal(untrustedBase.OPENAI_API_KEY, "sk-project-supplied", "일반 API 키는 프로젝트 dotenv로 허용");
66
- }
67
-
68
- // 신뢰 출처(사용자 전역 credentials.env/볼트)는 같은 키를 정당하게 설정할 수 있다 —
69
- // 로컬 Ollama·자체호스팅 엔드포인트를 쓰는 정상 사용자를 깨지 않는다.
70
- {
71
- const trustedBase = {};
72
- const trustedDotenv = runtime.parseDotEnvCli([
73
- "OLLAMA_HOST=http://127.0.0.1:11434",
74
- "AGENTLAS_MCP_BASE_URL=https://self-hosted.internal/mcp",
75
- ].join("\n"));
76
- runtime.mergeChildEnvValuesCli(trustedBase, trustedDotenv, false /* overwrite */, true /* trusted */);
77
- assert.equal(trustedBase.OLLAMA_HOST, "http://127.0.0.1:11434", "신뢰 출처는 OLLAMA_HOST 허용");
78
- assert.equal(trustedBase.AGENTLAS_MCP_BASE_URL, "https://self-hosted.internal/mcp", "신뢰 출처는 엔드포인트 허용");
79
- }
80
-
81
- // 호스트 신원 키(HOME/PATH/NODE_OPTIONS 등)는 신뢰 출처라도 절대 불가
82
- {
83
- const b = {};
84
- runtime.mergeChildEnvValuesCli(b, runtime.parseDotEnvCli("NODE_OPTIONS=--require=/x.js\nPATH=/evil"), true, true);
85
- assert.equal(b.NODE_OPTIONS, undefined, "NODE_OPTIONS는 신뢰 출처라도 불가");
86
- assert.equal(b.PATH, undefined, "PATH는 신뢰 출처라도 불가");
87
- }
88
-
89
- console.log(JSON.stringify({ ok: true, checks: 25 }, null, 2));
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const assert = require("node:assert/strict");
5
- const fs = require("node:fs");
6
- const path = require("node:path");
7
- const { compareSemVer, normalizeSemVer, parseSemVer } = require("../engine/semver.cjs");
8
-
9
- const precedence = [
10
- "1.0.0-alpha",
11
- "1.0.0-alpha.1",
12
- "1.0.0-alpha.beta",
13
- "1.0.0-beta",
14
- "1.0.0-beta.2",
15
- "1.0.0-beta.11",
16
- "1.0.0-rc.1",
17
- "1.0.0",
18
- ];
19
-
20
- for (let index = 0; index < precedence.length - 1; index += 1) {
21
- assert.equal(compareSemVer(precedence[index], precedence[index + 1]), -1);
22
- assert.equal(compareSemVer(precedence[index + 1], precedence[index]), 1);
23
- }
24
-
25
- assert.equal(compareSemVer("v2.3.4", "2.3.4"), 0);
26
- assert.equal(normalizeSemVer("v2.3.4-rc.1+build.7"), "2.3.4-rc.1+build.7");
27
- assert.equal(compareSemVer("1.0.0+build.1", "1.0.0+build.99"), 0);
28
- assert.equal(compareSemVer("1.0.0-1", "1.0.0-alpha"), -1);
29
- assert.equal(compareSemVer("999999999999999999999.0.0", "2.0.0"), 1);
30
- assert.equal(compareSemVer("1.0.0", "1.0.0-rc.99"), 1);
31
- assert.equal(parseSemVer("1.0.0-01"), null);
32
- assert.equal(parseSemVer("01.0.0"), null);
33
- assert.equal(compareSemVer("not-a-version", "1.0.0"), null);
34
-
35
- const updater = fs.readFileSync(path.join(__dirname, "../engine/agentlas.cjs"), "utf8");
36
- assert.match(updater, /compareSemVer\(currentVersion, latestVersion\)/);
37
- assert.doesNotMatch(updater, /function versionParts/);
38
-
39
- console.log("semver-precedence: PASS");
package/test/smoke.sh DELETED
@@ -1,93 +0,0 @@
1
- #!/bin/sh
2
- # agentlas 터미널 CLI 스모크 테스트.
3
- # 1) 기본(auto=bundled) where/version/list/doctor
4
- # 2) 신선 환경(빈 userData) 첫 실행: DB 부트스트랩 + 빌트인 시드
5
- set -eu
6
-
7
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
- BIN="$SCRIPT_DIR/../bin/agentlas.cjs"
9
-
10
- pass=0
11
- fail=0
12
- check() {
13
- name="$1"; shift
14
- if out="$("$@" 2>&1)"; then
15
- echo "PASS $name"
16
- pass=$((pass + 1))
17
- else
18
- echo "FAIL $name"
19
- echo "$out" | sed 's/^/ /'
20
- fail=$((fail + 1))
21
- fi
22
- }
23
-
24
- check "where" node "$BIN" --where
25
- check "version" node "$BIN" version
26
- check "list" node "$BIN" list
27
- check "doctor" node "$BIN" doctor
28
- check "help" node "$BIN" help
29
- check "usage" node "$BIN" usage
30
- check "mcp" node "$BIN" mcp
31
- check "chats" node "$BIN" chats
32
- check "run-api-regression" node "$SCRIPT_DIR/run-api-regression.cjs"
33
- check "cloud-runtime-paths" node "$SCRIPT_DIR/cloud-runtime-paths.cjs"
34
- check "cloud-save-publish" node "$SCRIPT_DIR/cloud-save-publish.cjs"
35
- check "cloud-asset-restore" node "$SCRIPT_DIR/cloud-asset-restore.cjs"
36
- check "cloud-owner-restore" node "$SCRIPT_DIR/cloud-owner-restore.cjs"
37
- check "cloud-cas-client" node "$SCRIPT_DIR/cloud-cas-client.cjs"
38
- check "runtime-env-protection" node "$SCRIPT_DIR/runtime-env-protection.cjs"
39
- check "credential-env-regression" node "$SCRIPT_DIR/credential-env-regression.cjs"
40
- check "tool-workspace-boundary" node "$SCRIPT_DIR/tool-workspace-boundary.cjs"
41
- check "mcp-config-isolation" node "$SCRIPT_DIR/mcp-config-isolation.cjs"
42
- check "experience-mcp-contract" node "$SCRIPT_DIR/experience-mcp-contract.cjs"
43
- check "experience-exchange-contract" node "$SCRIPT_DIR/experience-exchange-contract.cjs"
44
- check "bootstrap-race" node "$SCRIPT_DIR/bootstrap-race.cjs"
45
- check "login-loopback-security" node "$SCRIPT_DIR/login-loopback-security.cjs"
46
- check "timeout-regression" node "$SCRIPT_DIR/timeout-regression.cjs"
47
- check "terminal-ui-regression" node "$SCRIPT_DIR/terminal-ui-regression.cjs"
48
- check "route-regression" node "$SCRIPT_DIR/route-regression.cjs"
49
- check "engine-hardening-regression" node "$SCRIPT_DIR/engine-hardening-regression.cjs"
50
- check "permission-mapping" node "$SCRIPT_DIR/permission-mapping.cjs"
51
- check "sqlite-driver-probe" node "$SCRIPT_DIR/sqlite-driver-probe.cjs"
52
- check "capture-runtime-guard" node "$SCRIPT_DIR/capture-runtime-guard.cjs"
53
- check "update-safety" node "$SCRIPT_DIR/update-safety.cjs"
54
- check "semver-precedence" node "$SCRIPT_DIR/semver-precedence.cjs"
55
-
56
- # Agentlas OS 표면: 무인자 호출은 usage를 내고 exit 1 (프롬프트 오라우팅 방지 확인)
57
- guard() {
58
- name="$1"; shift
59
- if out="$("$@" 2>&1)"; then echo "FAIL $name (should exit non-zero)"; fail=$((fail + 1));
60
- else echo "PASS $name"; pass=$((pass + 1)); fi
61
- }
62
- guard "guard-search" node "$BIN" search
63
- guard "guard-install" node "$BIN" install
64
- guard "guard-upload" node "$BIN" upload
65
-
66
- # 신선 환경 첫 실행 (표준: mktemp 사용, 검증 후 Trash로 이동)
67
- FRESH="$(mktemp -d "${TMPDIR:-/tmp}/agentlas-smoke-XXXXXX")"
68
- check "fresh-first-run" env AGENTLAS_USER_DATA_DIR="$FRESH" node "$BIN" list
69
- if [ -f "$FRESH/agentlas.sqlite" ]; then
70
- echo "PASS fresh-db-created"
71
- pass=$((pass + 1))
72
- else
73
- echo "FAIL fresh-db-created"
74
- fail=$((fail + 1))
75
- fi
76
- mv "$FRESH" "$HOME/.Trash/agentlas-smoke-$(date +%s)" 2>/dev/null || true
77
-
78
- # Runtime Doctor 3제품 패리티 게이트 — 데스크탑 TS ↔ 이 repo CJS ↔ system-optimizer
79
- # 플레이북이 어긋나면 여기서 FAIL. (형제 repo가 없는 CI/신선 클론에선 자동 스킵)
80
- SYNC="$(cd "$(dirname "$0")/.." && pwd)/../scripts/sync-runtime-doctor.sh"
81
- if [ -f "$SYNC" ]; then
82
- if bash "$SYNC"; then
83
- echo "PASS doctor-parity"
84
- pass=$((pass + 1))
85
- else
86
- echo "FAIL doctor-parity"
87
- fail=$((fail + 1))
88
- fi
89
- fi
90
-
91
- echo ""
92
- echo "smoke: $pass passed, $fail failed"
93
- [ "$fail" -eq 0 ]
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const assert = require("node:assert/strict");
5
- const { spawnSync } = require("node:child_process");
6
- const path = require("node:path");
7
- const { probeSqliteDriver } = require("../bin/agentlas.cjs");
8
-
9
- const driver = probeSqliteDriver();
10
- assert.ok(driver === "better-sqlite3" || driver === "node:sqlite", `unexpected SQLite driver: ${driver}`);
11
-
12
- const launcher = path.join(__dirname, "..", "bin", "agentlas.cjs");
13
- const result = spawnSync(process.execPath, [launcher, "--where"], {
14
- encoding: "utf8",
15
- env: { ...process.env, NODE_NO_WARNINGS: "" },
16
- });
17
- assert.equal(result.status, 0, result.stderr || result.stdout);
18
- assert.doesNotMatch(result.stderr, /ExperimentalWarning|SQLite is an experimental feature/i);
19
- const where = JSON.parse(result.stdout);
20
- assert.equal(where.sqliteDriver, driver, "--where must report the driver that can actually open a database");
21
-
22
- console.log(`sqlite-driver-probe: PASS (${driver})`);