agentlas 0.6.0 → 0.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/README.md +73 -0
- package/engine/agentlas-capabilities.cjs +34 -3
- package/engine/agentlas-experience-exchange.cjs +1401 -0
- package/engine/agentlas-experience-mcp.cjs +1147 -0
- package/engine/agentlas-repl.cjs +21 -13
- package/engine/agentlas.cjs +281 -50
- package/package.json +1 -1
- package/test/cloud-save-publish.cjs +34 -0
- package/test/engine-hardening-regression.cjs +74 -0
- package/test/experience-exchange-contract.cjs +569 -0
- package/test/experience-mcp-contract.cjs +391 -0
- package/test/fixtures/portable-experience-bundle-v1-golden.json +124 -0
- package/test/route-regression.cjs +244 -8
- package/test/runtime-env-protection.cjs +45 -1
- package/test/smoke.sh +3 -0
- package/test/terminal-ui-regression.cjs +7 -2
|
@@ -156,6 +156,40 @@ function close(server) {
|
|
|
156
156
|
assert.equal(binary.executable, false);
|
|
157
157
|
assert.equal(privateBundle.files.find((file) => file.path === "run.sh")?.executable, true);
|
|
158
158
|
assert.equal(privateBundle.files.some((file) => file.path === ".agentlas-cloud-package.json"), false);
|
|
159
|
+
|
|
160
|
+
// Experience lineage is local, rebuildable, and owned separately from the
|
|
161
|
+
// immutable base Agent package. Canonical, backup, and crash-safe hidden
|
|
162
|
+
// temp siblings must neither ship nor perturb the base package hash.
|
|
163
|
+
const lineageDir = path.join(privateRoot, ".agentlas");
|
|
164
|
+
fs.mkdirSync(lineageDir, { recursive: true });
|
|
165
|
+
const lineagePaths = [
|
|
166
|
+
"experience-relations.jsonl",
|
|
167
|
+
"experience-relations.jsonl.previous",
|
|
168
|
+
".experience-relations.jsonl.1234.tmp",
|
|
169
|
+
".experience-relations.jsonl.tmp-recovery",
|
|
170
|
+
];
|
|
171
|
+
for (const [index, name] of lineagePaths.entries()) {
|
|
172
|
+
fs.writeFileSync(path.join(lineageDir, name), `private-lineage-${index}\n`, "utf8");
|
|
173
|
+
}
|
|
174
|
+
const lineageExcluded = await packageCloudAgentCli(null, privateRoot, { dryRun: true, llmReview: false });
|
|
175
|
+
const lineageBundle = JSON.parse(fs.readFileSync(lineageExcluded.bundlePath, "utf8"));
|
|
176
|
+
assert.equal(lineageBundle.manifest.packageHash, privateBundle.manifest.packageHash, "local lineage must not change the base hash");
|
|
177
|
+
for (const name of lineagePaths) {
|
|
178
|
+
assert.equal(lineageBundle.files.some((file) => file.path === `.agentlas/${name}`), false, `${name} must not ship`);
|
|
179
|
+
assert.equal(lineageExcluded.files.find((file) => file.path === `.agentlas/${name}`)?.reason, "experience-lineage-separate-asset");
|
|
180
|
+
fs.appendFileSync(path.join(lineageDir, name), "changed-without-base-release\n", "utf8");
|
|
181
|
+
}
|
|
182
|
+
const lineageChanged = await packageCloudAgentCli(null, privateRoot, { dryRun: true, llmReview: false });
|
|
183
|
+
assert.equal(lineageChanged.manifest.packageHash, privateBundle.manifest.packageHash, "lineage mutation must not create a new base release identity");
|
|
184
|
+
assert.equal(
|
|
185
|
+
cloudHashPackage([...privateBundle.files, {
|
|
186
|
+
path: ".agentlas/experience-relations.jsonl.previous",
|
|
187
|
+
sha256: "f".repeat(64),
|
|
188
|
+
executable: false,
|
|
189
|
+
}], privateBundle.manifest.packageHashVersion),
|
|
190
|
+
privateBundle.manifest.packageHash,
|
|
191
|
+
"hash helper must defensively omit local Experience lineage siblings",
|
|
192
|
+
);
|
|
159
193
|
assert.equal(
|
|
160
194
|
cloudPortablePathConflict(["Skills/writer/SKILL.md", "skills/reviewer/SKILL.md"])?.code,
|
|
161
195
|
"path-alias-collision",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/*
|
|
4
|
+
* bug-hunter(2026-07-12) 확정 결함 회귀 고정:
|
|
5
|
+
* 1) node:sqlite 폴백 래퍼 API 패리티 — exec/pragma 누락으로 ensureMemoryContextColumn이
|
|
6
|
+
* 조용히 죽어 context_json 마이그레이션이 안 되던 버그.
|
|
7
|
+
* 2) 민감 상태 파일 원자적·0600 쓰기 — 크래시 중간쓰기 JSON 손상 + world-readable 노출.
|
|
8
|
+
* 3) /install 중복 case — i18n 핸들러가 죽은 코드가 되던 버그(정적 검증).
|
|
9
|
+
* 4) gemini 2턴+ 시스템 프롬프트 소실(정적 검증: resume 게이트).
|
|
10
|
+
*/
|
|
11
|
+
const assert = require("node:assert/strict");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const os = require("node:os");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const engine = require("../engine/agentlas.cjs");
|
|
16
|
+
|
|
17
|
+
// ── 1) node:sqlite 폴백 래퍼: exec/pragma 존재 + ensureMemoryContextColumn 실동작 ──
|
|
18
|
+
{
|
|
19
|
+
const tmpDb = path.join(os.tmpdir(), `agentlas-wrap-${process.pid}.sqlite`);
|
|
20
|
+
try { fs.unlinkSync(tmpDb); } catch { /* fresh */ }
|
|
21
|
+
const db = engine.openNodeSqliteDb(tmpDb);
|
|
22
|
+
assert.equal(typeof db.exec, "function", "폴백 래퍼에 exec가 있어야 함");
|
|
23
|
+
assert.equal(typeof db.pragma, "function", "폴백 래퍼에 pragma가 있어야 함");
|
|
24
|
+
|
|
25
|
+
// 앱 마이그레이션 전(context_json 없는) memory_entries를 만든 뒤 ensureMemoryContextColumn 실행
|
|
26
|
+
db.exec("CREATE TABLE memory_entries (id INTEGER PRIMARY KEY, kind TEXT, content TEXT)");
|
|
27
|
+
engine.ensureMemoryContextColumn(db); // 예전 버그: db.exec undefined → TypeError를 try/catch가 삼킴
|
|
28
|
+
const cols = db.prepare("PRAGMA table_info(memory_entries)").all().map((c) => c.name);
|
|
29
|
+
assert.ok(cols.includes("context_json"), `context_json 컬럼이 추가돼야 함 — 실제: ${cols.join(",")}`);
|
|
30
|
+
|
|
31
|
+
// pragma() 스칼라 근사 관례
|
|
32
|
+
const uv = db.pragma("user_version");
|
|
33
|
+
assert.ok(typeof uv === "number" || typeof uv === "bigint", `pragma 스칼라 반환 — 실제: ${typeof uv}`);
|
|
34
|
+
db.close();
|
|
35
|
+
try { fs.unlinkSync(tmpDb); } catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── 2) writeJsonPrivateAtomicCli: 유효 JSON + 0600 + 원자성(temp 잔여물 없음) ──
|
|
39
|
+
{
|
|
40
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-priv-"));
|
|
41
|
+
const target = path.join(dir, "cli-sessions.json");
|
|
42
|
+
const value = [{ kind: "claude-code", sessionId: "s-123", cwd: "/x" }];
|
|
43
|
+
engine.writeJsonPrivateAtomicCli(target, value);
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(target, "utf8")), value, "쓴 JSON을 그대로 읽어야 함");
|
|
46
|
+
if (process.platform !== "win32") {
|
|
47
|
+
const mode = fs.statSync(target).mode & 0o777;
|
|
48
|
+
assert.equal(mode, 0o600, `민감 파일은 0600 — 실제: 0o${mode.toString(8)}`);
|
|
49
|
+
}
|
|
50
|
+
// temp 잔여물이 남지 않아야 함
|
|
51
|
+
const leftovers = fs.readdirSync(dir).filter((n) => n.includes(".tmp"));
|
|
52
|
+
assert.equal(leftovers.length, 0, `temp 잔여물 없어야 함 — 실제: ${leftovers.join(",")}`);
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 3) /install 중복 case 제거(정적) — handleSlash에 case "install"은 정확히 하나 ──
|
|
57
|
+
{
|
|
58
|
+
const repl = fs.readFileSync(path.join(__dirname, "..", "engine", "agentlas-repl.cjs"), "utf8");
|
|
59
|
+
const count = (repl.match(/case "install":/g) || []).length;
|
|
60
|
+
assert.equal(count, 1, `case "install"은 하나여야 함(중복 시 i18n 핸들러가 죽은 코드) — 실제: ${count}`);
|
|
61
|
+
// 살아남은 핸들러는 i18n 키를 쓴다(하드코딩 한국어 아님)
|
|
62
|
+
assert.ok(/ui\.t\("installUsage"\)/.test(repl), "살아있는 install 핸들러는 installUsage i18n 키 사용");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 4) gemini 시스템 프롬프트 유지(정적) — resume 게이트가 claude/codex로 한정 ──
|
|
66
|
+
{
|
|
67
|
+
const repl = fs.readFileSync(path.join(__dirname, "..", "engine", "agentlas-repl.cjs"), "utf8");
|
|
68
|
+
assert.ok(/resumesServerSide\s*=\s*rt\.kind\s*===\s*"claude-code"\s*\|\|\s*rt\.kind\s*===\s*"codex"/.test(repl),
|
|
69
|
+
"resume 게이트는 claude-code/codex로 한정돼야 함(gemini는 매 턴 시스템 프롬프트 재전송)");
|
|
70
|
+
assert.ok(/systemPrompt:\s*session\.id\s*&&\s*resumesServerSide\s*\?\s*""\s*:\s*sys/.test(repl),
|
|
71
|
+
"gemini는 session.id가 있어도 시스템 프롬프트를 비우지 않아야 함");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("engine-hardening-regression: PASS");
|