feat: enforce @scope/name format for all variable names #30

Merged
xiaomo merged 1 commits from feat/29-scoped-variable-names into main 2026-06-01 16:15:26 +00:00
11 changed files with 368 additions and 365 deletions
+6 -2
View File
@@ -621,7 +621,9 @@ async function cmdVarSet(args: string[]): Promise<void> {
}
if (name.startsWith("@ocas/")) {
die("The @ocas/ namespace is reserved and cannot be modified directly.");
die(
"The @ocas/ namespace is reserved and cannot be modified directly. Use a different scope, e.g. @myapp/name (variable names must follow @scope/name format).",
);
}
const store = await openStore();
@@ -704,7 +706,9 @@ async function cmdVarDelete(args: string[]): Promise<void> {
}
if (name.startsWith("@ocas/")) {
die("The @ocas/ namespace is reserved and cannot be modified directly.");
die(
"The @ocas/ namespace is reserved and cannot be modified directly. Use a different scope, e.g. @myapp/name (variable names must follow @scope/name format).",
);
}
const { store, varStore } = await openStoreAndVarStore();
@@ -4,7 +4,7 @@ exports[`Phase 7: Edge Cases 7.1 get non-existent hash errors gracefully 1`] = `
exports[`Phase 7: Edge Cases 7.3 var set empty name errors 1`] = `"Usage: ocas var set <name> <hash> [--tag <tag>...]"`;
exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Segment "invalid name!" contains invalid characters (only @, a-z, A-Z, 0-9, ., _, - allowed)"`;
exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"Error: Invalid variable name "invalid name!": Name must follow @scope/name format (e.g. @myapp/config)"`;
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
"Usage: ocas [--home <path>] [--json] <command> [args]
@@ -57,7 +57,7 @@ exports[`Phase 3: Variable System 3.1 var set creates variable 1`] = `
"type": "0Q5EMYK4SYSS9",
"value": {
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {},
"value": "9W3MGR3184QYE",
@@ -70,7 +70,7 @@ exports[`Phase 3: Variable System 3.2 var get returns variable 1`] = `
"type": "7C75FQT98KKQD",
"value": {
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {},
"value": "9W3MGR3184QYE",
@@ -294,7 +294,7 @@ exports[`Phase 3: Variable System 3.3 var list shows all variables 1`] = `
},
{
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {},
"value": "9W3MGR3184QYE",
@@ -309,7 +309,7 @@ exports[`Phase 3: Variable System 3.4 var list prefix filters by prefix 1`] = `
"value": [
{
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {},
"value": "9W3MGR3184QYE",
@@ -323,7 +323,7 @@ exports[`Phase 3: Variable System 3.5 var set upsert updates existing variable 1
"type": "0Q5EMYK4SYSS9",
"value": {
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {},
"value": "A6QPKJAFR68NP",
@@ -338,7 +338,7 @@ exports[`Phase 3: Variable System 3.6 var tag adds kv tag and label 1`] = `
"labels": [
"important",
],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {
"env": "prod",
@@ -356,7 +356,7 @@ exports[`Phase 3: Variable System 3.7 var list --tag env:prod filters by kv tag
"labels": [
"important",
],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {
"env": "prod",
@@ -375,7 +375,7 @@ exports[`Phase 3: Variable System 3.8 var list --tag important filters by label
"labels": [
"important",
],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {
"env": "prod",
@@ -391,7 +391,7 @@ exports[`Phase 3: Variable System 3.9 var tag remove deletes label 1`] = `
"type": "9103EYRMM949A",
"value": {
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {
"env": "prod",
@@ -407,7 +407,7 @@ exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
"value": [
{
"labels": [],
"name": "myapp/config",
"name": "@myapp/config",
"schema": "FRBAB1BF0ZBCS",
"tags": {
"env": "prod",
@@ -418,7 +418,7 @@ exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
}
`;
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=myapp/config, schema=FRBAB1BF0ZBCS"`;
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=@myapp/config, schema=FRBAB1BF0ZBCS"`;
exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
{
+14 -14
View File
@@ -210,7 +210,7 @@ describe("Phase 3: Variable System", () => {
const { exitCode, stdout } = await runCli([
"var",
"set",
"myapp/config",
"@myapp/config",
nodeHash,
]);
expect(exitCode).toBe(0);
@@ -221,7 +221,7 @@ describe("Phase 3: Variable System", () => {
const { stdout, exitCode } = await runCli([
"var",
"get",
"myapp/config",
"@myapp/config",
"--schema",
typeHash,
]);
@@ -234,14 +234,14 @@ describe("Phase 3: Variable System", () => {
const { stdout, exitCode } = await runCli(["var", "list"]);
expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot();
expect(stdout).toContain("myapp/config");
expect(stdout).toContain("@myapp/config");
});
test("3.4 var list prefix filters by prefix", async () => {
const { stdout, exitCode } = await runCli(["var", "list", "myapp/"]);
const { stdout, exitCode } = await runCli(["var", "list", "@myapp/"]);
expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot();
expect(stdout).toContain("myapp/config");
expect(stdout).toContain("@myapp/config");
});
test("3.5 var set upsert updates existing variable", async () => {
@@ -252,20 +252,20 @@ describe("Phase 3: Variable System", () => {
const { exitCode, stdout } = await runCli([
"var",
"set",
"myapp/config",
"@myapp/config",
node2Hash,
]);
expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot();
// Restore original value
await runCli(["var", "set", "myapp/config", nodeHash]);
await runCli(["var", "set", "@myapp/config", nodeHash]);
});
test("3.6 var tag adds kv tag and label", async () => {
const { exitCode, stdout } = await runCli([
"var",
"tag",
"myapp/config",
"@myapp/config",
"--schema",
typeHash,
"env:prod",
@@ -283,7 +283,7 @@ describe("Phase 3: Variable System", () => {
"env:prod",
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("myapp/config");
expect(stdout).toContain("@myapp/config");
expect(stripVolatile(stdout)).toMatchSnapshot();
});
@@ -295,7 +295,7 @@ describe("Phase 3: Variable System", () => {
"important",
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("myapp/config");
expect(stdout).toContain("@myapp/config");
expect(stripVolatile(stdout)).toMatchSnapshot();
});
@@ -303,7 +303,7 @@ describe("Phase 3: Variable System", () => {
const { exitCode, stdout } = await runCli([
"var",
"tag",
"myapp/config",
"@myapp/config",
"--schema",
typeHash,
":important",
@@ -317,14 +317,14 @@ describe("Phase 3: Variable System", () => {
"--tag",
"important",
]);
expect(listOut).not.toContain("myapp/config");
expect(listOut).not.toContain("@myapp/config");
});
test("3.10 var delete removes variable", async () => {
const { exitCode, stdout } = await runCli([
"var",
"delete",
"myapp/config",
"@myapp/config",
]);
expect(exitCode).toBe(0);
expect(stripVolatile(stdout)).toMatchSnapshot();
@@ -334,7 +334,7 @@ describe("Phase 3: Variable System", () => {
const { stderr, exitCode } = await runCli([
"var",
"get",
"myapp/config",
"@myapp/config",
"--schema",
typeHash,
]);
+1 -1
View File
@@ -39,7 +39,7 @@ beforeAll(async () => {
nodeHash = envValue(stdout) as string;
// Set a var referencing the node so it survives GC
await runCli(["var", "set", "gc-test/ref", nodeHash]);
await runCli(["var", "set", "@test/gc-test/ref", nodeHash]);
});
afterAll(() => {
+11 -11
View File
@@ -190,55 +190,55 @@ describe("CLI var list pagination", () => {
test("G1./G2. --limit and --offset on var list", async () => {
for (let i = 0; i < 4; i++) {
const h = await putString(`tval-${i}`);
await runCli(["var", "set", `myvar-${i}`, h], storePath);
await runCli(["var", "set", `@test/myvar-${i}`, h], storePath);
await new Promise((r) => setTimeout(r, 2));
}
const { stdout: lim } = await runCli(
["var", "list", "myvar-", "--limit", "2"],
["var", "list", "@test/myvar-", "--limit", "2"],
storePath,
);
expect((envValue(lim) as unknown[]).length).toBe(2);
const { stdout: off } = await runCli(
["var", "list", "myvar-", "--offset", "1", "--limit", "10"],
["var", "list", "@test/myvar-", "--offset", "1", "--limit", "10"],
storePath,
);
const offList = envValue(off) as Array<{ name: string }>;
expect(offList.length).toBe(3);
expect(offList[0]?.name).toBe("myvar-1");
expect(offList[0]?.name).toBe("@test/myvar-1");
});
test("G3. --desc reverses var list order", async () => {
for (let i = 0; i < 3; i++) {
const h = await putString(`dval-${i}`);
await runCli(["var", "set", `dv-${i}`, h], storePath);
await runCli(["var", "set", `@test/dv-${i}`, h], storePath);
await new Promise((r) => setTimeout(r, 2));
}
const { stdout } = await runCli(
["var", "list", "dv-", "--desc"],
["var", "list", "@test/dv-", "--desc"],
storePath,
);
const list = envValue(stdout) as Array<{ name: string }>;
expect(list[0]?.name).toBe("dv-2");
expect(list[0]?.name).toBe("@test/dv-2");
});
test("G4. --sort updated", async () => {
for (let i = 0; i < 3; i++) {
const h = await putString(`sval-${i}`);
await runCli(["var", "set", `sv-${i}`, h], storePath);
await runCli(["var", "set", `@test/sv-${i}`, h], storePath);
await new Promise((r) => setTimeout(r, 2));
}
// Re-set sv-0 with NEW value to bump updated
await new Promise((r) => setTimeout(r, 2));
const newH = await putString("sval-0-new");
await runCli(["var", "set", "sv-0", newH], storePath);
await runCli(["var", "set", "@test/sv-0", newH], storePath);
const { stdout } = await runCli(
["var", "list", "sv-", "--sort", "updated"],
["var", "list", "@test/sv-", "--sort", "updated"],
storePath,
);
const list = envValue(stdout) as Array<{ name: string }>;
expect(list[list.length - 1]?.name).toBe("sv-0");
expect(list[list.length - 1]?.name).toBe("@test/sv-0");
});
test("G6. invalid --sort exits non-zero", async () => {
+10 -10
View File
@@ -87,13 +87,13 @@ describe("var history", () => {
const { schema, values } = await setupSchemaAndValues();
const v1 = values[0] as Hash;
let r = await runCli("var", "set", "x", v1);
let r = await runCli("var", "set", "@test/x", v1);
expect(r.exitCode).toBe(0);
r = await runCli("var", "history", "x", "--schema", schema);
r = await runCli("var", "history", "@test/x", "--schema", schema);
expect(r.exitCode).toBe(0);
const envelope = JSON.parse(r.stdout);
expect(envelope.value.name).toBe("x");
expect(envelope.value.name).toBe("@test/x");
expect(envelope.value.schema).toBe(schema);
expect(envelope.value.values).toEqual([v1]);
});
@@ -102,11 +102,11 @@ describe("var history", () => {
const { schema, values } = await setupSchemaAndValues();
const [v1, v2, v3] = values as [Hash, Hash, Hash, Hash];
await runCli("var", "set", "x", v1);
await runCli("var", "set", "x", v2);
await runCli("var", "set", "x", v3);
await runCli("var", "set", "@test/x", v1);
await runCli("var", "set", "@test/x", v2);
await runCli("var", "set", "@test/x", v3);
const r = await runCli("var", "history", "x", "--schema", schema);
const r = await runCli("var", "history", "@test/x", "--schema", schema);
expect(r.exitCode).toBe(0);
const envelope = JSON.parse(r.stdout);
expect(envelope.value.values).toEqual([v3, v2, v1]);
@@ -116,10 +116,10 @@ describe("var history", () => {
const { schema: _schema, values } = await setupSchemaAndValues();
const [v1, v2] = values as [Hash, Hash, Hash, Hash];
await runCli("var", "set", "x", v1);
await runCli("var", "set", "x", v2);
await runCli("var", "set", "@test/x", v1);
await runCli("var", "set", "@test/x", v2);
const r = await runCli("var", "history", "x");
const r = await runCli("var", "history", "@test/x");
expect(r.exitCode).toBe(0);
const envelope = JSON.parse(r.stdout);
expect(envelope.value.values).toEqual([v2, v1]);
+86 -86
View File
@@ -105,7 +105,7 @@ describe("var set", () => {
const { stdout, stderr, exitCode } = await runCli(
"var",
"set",
"workflow/config/agent",
"@test/workflow/config/agent",
hash,
);
@@ -115,7 +115,7 @@ describe("var set", () => {
const envelope = JSON.parse(stdout);
expect(envelope).toHaveProperty("type");
expect(envelope).toHaveProperty("value");
expect(envelope.value.name).toBe("workflow/config/agent");
expect(envelope.value.name).toBe("@test/workflow/config/agent");
expect(envelope.value.schema).toBe(typeHash);
expect(envelope.value.value).toBe(hash);
expect(envelope.value.tags).toEqual({});
@@ -132,7 +132,7 @@ describe("var set", () => {
const { stdout, exitCode } = await runCli(
"var",
"set",
"my/var",
"@test/my/var",
hash,
"--tag",
"env:prod",
@@ -155,7 +155,7 @@ describe("var set", () => {
const { stdout, exitCode } = await runCli(
"var",
"set",
"my/var",
"@test/my/var",
hash,
"--tag",
"stable",
@@ -177,13 +177,13 @@ describe("var set", () => {
const hash2 = await createTestNode(store, typeHash, { test: "data2" });
// Create initial variable
await runCli("var", "set", "config", hash1);
await runCli("var", "set", "@test/config", hash1);
// Wait a bit to ensure updated > created
await new Promise((resolve) => setTimeout(resolve, 10));
// Update with hash2
const { stdout, exitCode } = await runCli("var", "set", "config", hash2);
const { stdout, exitCode } = await runCli("var", "set", "@test/config", hash2);
expect(exitCode).toBe(0);
@@ -200,10 +200,10 @@ describe("var set", () => {
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
// Create first variant
await runCli("var", "set", "config", hash1);
await runCli("var", "set", "@test/config", hash1);
// Create second variant with different schema
const { stdout, exitCode } = await runCli("var", "set", "config", hash2);
const { stdout, exitCode } = await runCli("var", "set", "@test/config", hash2);
expect(exitCode).toBe(0);
@@ -211,7 +211,7 @@ describe("var set", () => {
expect(envelope.value.schema).toBe(typeHash2);
// Verify both variants exist
const { stdout: listOut } = await runCli("var", "list", "config");
const { stdout: listOut } = await runCli("var", "list", "@test/config");
const listEnvelope = JSON.parse(listOut);
expect(listEnvelope.value.length).toBe(2);
});
@@ -222,13 +222,13 @@ describe("var set", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create with initial tags/labels
await runCli("var", "set", "x", hash, "--tag", "a:1", "--tag", "b");
await runCli("var", "set", "@test/x", hash, "--tag", "a:1", "--tag", "b");
// Update with new tags
const { stdout, exitCode } = await runCli(
"var",
"set",
"x",
"@test/x",
hash,
"--tag",
"c:2",
@@ -245,7 +245,7 @@ describe("var set", () => {
const { stderr, exitCode } = await runCli(
"var",
"set",
"my/var",
"@test/my/var",
"NONEXISTENT_HASH",
);
@@ -287,7 +287,7 @@ describe("var set", () => {
const { stderr, exitCode } = await runCli(
"var",
"set",
"x",
"@test/x",
hash,
"--tag",
"env:prod",
@@ -307,13 +307,13 @@ describe("var get", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable first
await runCli("var", "set", "config", hash);
await runCli("var", "set", "@test/config", hash);
// Get it back
const { stdout, exitCode } = await runCli(
"var",
"get",
"config",
"@test/config",
"--schema",
typeHash,
);
@@ -321,7 +321,7 @@ describe("var get", () => {
expect(exitCode).toBe(0);
const envelope = JSON.parse(stdout);
expect(envelope.value.name).toBe("config");
expect(envelope.value.name).toBe("@test/config");
expect(envelope.value.schema).toBe(typeHash);
});
@@ -332,19 +332,19 @@ describe("var get", () => {
const { stderr, exitCode } = await runCli(
"var",
"get",
"nonexistent",
"@test/nonexistent",
"--schema",
typeHash,
);
expect(exitCode).toBe(1);
expect(stderr).toContain(
`Error: Variable not found: name=nonexistent, schema=${typeHash}`,
`Error: Variable not found: name=@test/nonexistent, schema=${typeHash}`,
);
});
test("error when --schema missing", async () => {
const { stderr, exitCode } = await runCli("var", "get", "config");
const { stderr, exitCode } = await runCli("var", "get", "@test/config");
expect(exitCode).toBe(1);
expect(stderr).toContain(
@@ -360,17 +360,17 @@ describe("var get", () => {
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
// Create two variants
await runCli("var", "set", "config", hash1);
await runCli("var", "set", "config", hash2);
await runCli("var", "set", "@test/config", hash1);
await runCli("var", "set", "@test/config", hash2);
// Get first variant
const result1 = await runCli("var", "get", "config", "--schema", typeHash1);
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
expect(result1.exitCode).toBe(0);
const envelope1 = JSON.parse(result1.stdout);
expect(envelope1.value.value).toBe(hash1);
// Get second variant
const result2 = await runCli("var", "get", "config", "--schema", typeHash2);
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
expect(result2.exitCode).toBe(0);
const envelope2 = JSON.parse(result2.stdout);
expect(envelope2.value.value).toBe(hash2);
@@ -386,11 +386,11 @@ describe("var delete", () => {
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
// Create two variants
await runCli("var", "set", "config", hash1);
await runCli("var", "set", "config", hash2);
await runCli("var", "set", "@test/config", hash1);
await runCli("var", "set", "@test/config", hash2);
// Delete all
const { stdout, exitCode } = await runCli("var", "delete", "config");
const { stdout, exitCode } = await runCli("var", "delete", "@test/config");
expect(exitCode).toBe(0);
@@ -399,10 +399,10 @@ describe("var delete", () => {
expect(envelope.value.length).toBe(2);
// Verify both are deleted
const result1 = await runCli("var", "get", "config", "--schema", typeHash1);
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
expect(result1.exitCode).toBe(1);
const result2 = await runCli("var", "get", "config", "--schema", typeHash2);
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
expect(result2.exitCode).toBe(1);
});
@@ -414,14 +414,14 @@ describe("var delete", () => {
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
// Create two variants
await runCli("var", "set", "config", hash1);
await runCli("var", "set", "config", hash2);
await runCli("var", "set", "@test/config", hash1);
await runCli("var", "set", "@test/config", hash2);
// Delete only first variant
const { stdout, exitCode } = await runCli(
"var",
"delete",
"config",
"@test/config",
"--schema",
typeHash1,
);
@@ -432,16 +432,16 @@ describe("var delete", () => {
expect(envelope.value.schema).toBe(typeHash1);
// Verify first is deleted
const result1 = await runCli("var", "get", "config", "--schema", typeHash1);
const result1 = await runCli("var", "get", "@test/config", "--schema", typeHash1);
expect(result1.exitCode).toBe(1);
// Verify second still exists
const result2 = await runCli("var", "get", "config", "--schema", typeHash2);
const result2 = await runCli("var", "get", "@test/config", "--schema", typeHash2);
expect(result2.exitCode).toBe(0);
});
test("return empty array when name not found", async () => {
const { stdout, exitCode } = await runCli("var", "delete", "nonexistent");
const { stdout, exitCode } = await runCli("var", "delete", "@test/nonexistent");
expect(exitCode).toBe(0);
@@ -454,14 +454,14 @@ describe("var delete", () => {
const { stderr, exitCode } = await runCli(
"var",
"delete",
"config",
"@test/config",
"--schema",
"00000000000ZZ",
);
expect(exitCode).toBe(1);
expect(stderr).toContain(
"Error: Variable not found: name=config, schema=00000000000ZZ",
"Error: Variable not found: name=@test/config, schema=00000000000ZZ",
);
});
@@ -471,13 +471,13 @@ describe("var delete", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable with tags and labels
await runCli("var", "set", "x", hash, "--tag", "a:1", "--tag", "b");
await runCli("var", "set", "@test/x", hash, "--tag", "a:1", "--tag", "b");
// Delete it
const { exitCode } = await runCli(
"var",
"delete",
"x",
"@test/x",
"--schema",
typeHash,
);
@@ -485,7 +485,7 @@ describe("var delete", () => {
expect(exitCode).toBe(0);
// Verify it's gone
const result = await runCli("var", "get", "x", "--schema", typeHash);
const result = await runCli("var", "get", "@test/x", "--schema", typeHash);
expect(result.exitCode).toBe(1);
});
});
@@ -500,12 +500,12 @@ describe("var list", () => {
// Create three variables (use a prefix to filter out builtin @ocas/* vars
// that bootstrap writes into the varStore)
await runCli("var", "set", "test/a", hash1);
await runCli("var", "set", "test/b", hash2);
await runCli("var", "set", "test/c", hash3);
await runCli("var", "set", "@test/test/a", hash1);
await runCli("var", "set", "@test/test/b", hash2);
await runCli("var", "set", "@test/test/c", hash3);
// List all under our prefix
const { stdout, exitCode } = await runCli("var", "list", "test/");
const { stdout, exitCode } = await runCli("var", "list", "@test/test/");
expect(exitCode).toBe(0);
@@ -522,19 +522,19 @@ describe("var list", () => {
const hash3 = await createTestNode(store, typeHash, { test: "data3" });
// Create variables with different prefixes
await runCli("var", "set", "workflow/config/agent", hash1);
await runCli("var", "set", "workflow/config/model", hash2);
await runCli("var", "set", "other/var", hash3);
await runCli("var", "set", "@test/workflow/config/agent", hash1);
await runCli("var", "set", "@test/workflow/config/model", hash2);
await runCli("var", "set", "@test/other/var", hash3);
// List with prefix
const { stdout, exitCode } = await runCli("var", "list", "workflow/");
const { stdout, exitCode } = await runCli("var", "list", "@test/workflow/");
expect(exitCode).toBe(0);
const envelope = JSON.parse(stdout);
expect(envelope.value.length).toBe(2);
expect(envelope.value[0].name).toContain("workflow/");
expect(envelope.value[1].name).toContain("workflow/");
expect(envelope.value[0].name).toContain("@test/workflow/");
expect(envelope.value[1].name).toContain("@test/workflow/");
});
test("filter by schema", async () => {
@@ -554,8 +554,8 @@ describe("var list", () => {
void bootstrapHash;
// Create variables with different schemas
await runCli("var", "set", "a", hash1);
await runCli("var", "set", "b", hash2);
await runCli("var", "set", "@test/a", hash1);
await runCli("var", "set", "@test/b", hash2);
// List with schema filter
const { stdout, exitCode } = await runCli(
@@ -583,7 +583,7 @@ describe("var list", () => {
await runCli(
"var",
"set",
"a",
"@test/a",
hash1,
"--tag",
"env:prod",
@@ -593,14 +593,14 @@ describe("var list", () => {
await runCli(
"var",
"set",
"b",
"@test/b",
hash2,
"--tag",
"env:prod",
"--tag",
"region:eu",
);
await runCli("var", "set", "c", hash3, "--tag", "env:dev");
await runCli("var", "set", "@test/c", hash3, "--tag", "env:dev");
// List with tag filter (AND logic)
const { stdout, exitCode } = await runCli(
@@ -616,7 +616,7 @@ describe("var list", () => {
const envelope = JSON.parse(stdout);
expect(envelope.value.length).toBe(1);
expect(envelope.value[0].name).toBe("a");
expect(envelope.value[0].name).toBe("@test/a");
});
test("filter by labels", async () => {
@@ -629,14 +629,14 @@ describe("var list", () => {
await runCli(
"var",
"set",
"a",
"@test/a",
hash1,
"--tag",
"stable",
"--tag",
"production",
);
await runCli("var", "set", "b", hash2, "--tag", "stable");
await runCli("var", "set", "@test/b", hash2, "--tag", "stable");
// List with single label
const result1 = await runCli("var", "list", "--tag", "stable");
@@ -656,7 +656,7 @@ describe("var list", () => {
expect(result2.exitCode).toBe(0);
envelope = JSON.parse(result2.stdout);
expect(envelope.value.length).toBe(1);
expect(envelope.value[0].name).toBe("a");
expect(envelope.value[0].name).toBe("@test/a");
});
test("combined filters", async () => {
@@ -667,15 +667,15 @@ describe("var list", () => {
const hash3 = await createTestNode(store, typeHash1, { test: "data3" });
// Create variables
await runCli("var", "set", "workflow/a", hash1, "--tag", "env:prod");
await runCli("var", "set", "workflow/b", hash2, "--tag", "env:dev");
await runCli("var", "set", "other/c", hash3, "--tag", "env:prod");
await runCli("var", "set", "@test/workflow/a", hash1, "--tag", "env:prod");
await runCli("var", "set", "@test/workflow/b", hash2, "--tag", "env:dev");
await runCli("var", "set", "@test/other/c", hash3, "--tag", "env:prod");
// List with combined filters
const { stdout, exitCode } = await runCli(
"var",
"list",
"workflow/",
"@test/workflow/",
"--schema",
typeHash1,
"--tag",
@@ -686,14 +686,14 @@ describe("var list", () => {
const envelope = JSON.parse(stdout);
expect(envelope.value.length).toBe(1);
expect(envelope.value[0].name).toBe("workflow/a");
expect(envelope.value[0].name).toBe("@test/workflow/a");
});
test("empty result when no matches", async () => {
const { stdout, exitCode } = await runCli(
"var",
"list",
"nonexistent/prefix/",
"@test/nonexistent/prefix/",
);
expect(exitCode).toBe(0);
@@ -720,13 +720,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable without tags
await runCli("var", "set", "x", hash);
await runCli("var", "set", "@test/x", hash);
// Add tag
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
"env:prod",
@@ -744,13 +744,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable with tag
await runCli("var", "set", "x", hash, "--tag", "env:dev");
await runCli("var", "set", "@test/x", hash, "--tag", "env:dev");
// Update tag
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
"env:prod",
@@ -768,13 +768,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable without labels
await runCli("var", "set", "x", hash);
await runCli("var", "set", "@test/x", hash);
// Add label
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
"stable",
@@ -795,7 +795,7 @@ describe("var tag", () => {
await runCli(
"var",
"set",
"x",
"@test/x",
hash,
"--tag",
"env:prod",
@@ -807,7 +807,7 @@ describe("var tag", () => {
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
":env",
@@ -825,13 +825,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable with labels
await runCli("var", "set", "x", hash, "--tag", "stable", "--tag", "beta");
await runCli("var", "set", "@test/x", hash, "--tag", "stable", "--tag", "beta");
// Delete label
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
":stable",
@@ -849,13 +849,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable with tags and labels
await runCli("var", "set", "x", hash, "--tag", "a:1", "--tag", "b");
await runCli("var", "set", "@test/x", hash, "--tag", "a:1", "--tag", "b");
// Mixed operations
const { stdout, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
"c:3",
@@ -876,13 +876,13 @@ describe("var tag", () => {
const hash = await createTestNode(store, typeHash, { test: "data" });
// Create variable with tag
await runCli("var", "set", "x", hash, "--tag", "env:prod");
await runCli("var", "set", "@test/x", hash, "--tag", "env:prod");
// Try to add same name as label
const { stderr, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
"env",
@@ -899,7 +899,7 @@ describe("var tag", () => {
const { stderr, exitCode } = await runCli(
"var",
"tag",
"nonexistent",
"@test/nonexistent",
"--schema",
typeHash,
"env:prod",
@@ -907,12 +907,12 @@ describe("var tag", () => {
expect(exitCode).toBe(1);
expect(stderr).toContain(
`Error: Variable not found: name=nonexistent, schema=${typeHash}`,
`Error: Variable not found: name=@test/nonexistent, schema=${typeHash}`,
);
});
test("error when --schema missing", async () => {
const { stderr, exitCode } = await runCli("var", "tag", "x", "env:prod");
const { stderr, exitCode } = await runCli("var", "tag", "@test/x", "env:prod");
expect(exitCode).toBe(1);
expect(stderr).toContain(
@@ -927,7 +927,7 @@ describe("var tag", () => {
const { stderr, exitCode } = await runCli(
"var",
"tag",
"x",
"@test/x",
"--schema",
typeHash,
);
@@ -945,13 +945,13 @@ describe("global options", () => {
const typeHash = await getBootstrapHash(store);
const hash = await createTestNode(store, typeHash, { test: "data" });
await runCli("var", "set", "x", hash);
await runCli("var", "set", "@test/x", hash);
const { stdout } = await runCli(
"--json",
"var",
"get",
"x",
"@test/x",
"--schema",
typeHash,
);
@@ -981,7 +981,7 @@ describe("global options", () => {
varDbPath,
"var",
"set",
"x",
"@test/x",
hash,
],
{
@@ -1012,7 +1012,7 @@ describe("global options", () => {
customDbPath,
"var",
"set",
"x",
"@test/x",
hash,
],
{
+12 -12
View File
@@ -39,7 +39,7 @@ describe("GC - Variable Model Refactoring", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashRef);
varStore.set("@test/config", hashRef);
const stats = gc(store, varStore);
@@ -66,8 +66,8 @@ describe("GC - Variable Model Refactoring", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashA);
varStore.set("config", hashB);
varStore.set("@test/config", hashA);
varStore.set("@test/config", hashB);
const stats = gc(store, varStore);
@@ -90,8 +90,8 @@ describe("GC - Variable Model Refactoring", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashRef);
varStore.remove("config", schemaHash);
varStore.set("@test/config", hashRef);
varStore.remove("@test/config", schemaHash);
const stats = gc(store, varStore);
@@ -117,9 +117,9 @@ describe("GC - Variable Model Refactoring", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("uwf.thread", hash1);
varStore.set("uwf.workflow", hash2);
varStore.set("app.config", hash3);
varStore.set("@test/uwf.thread", hash1);
varStore.set("@test/uwf.workflow", hash2);
varStore.set("@test/app.config", hash3);
const stats = gc(store, varStore);
@@ -151,9 +151,9 @@ describe("GC - Variable Model Refactoring", () => {
const varStore = new VariableStore(dbPath, store);
// Create variables
varStore.set("var1", hashA1);
varStore.set("var2", hashA2);
varStore.set("var3", hashB);
varStore.set("@test/var1", hashA1);
varStore.set("@test/var2", hashA2);
varStore.set("@test/var3", hashB);
// First GC: orphans removed
let stats = gc(store, varStore);
@@ -165,7 +165,7 @@ describe("GC - Variable Model Refactoring", () => {
expect(stats.scanned).toBe(3);
// Delete one variable
varStore.remove("var2", schemaAHash);
varStore.remove("@test/var2", schemaAHash);
// Second GC: hashA2 removed
stats = gc(store, varStore);
@@ -31,7 +31,7 @@ async function setN(prefix: string, n: number, delayMs = 2): Promise<Hash[]> {
const hashes: Hash[] = [];
for (let i = 0; i < n; i++) {
const h = await casStore.put(stringHash, `${prefix}-${i}`);
varStore.set(`${prefix}-${i}`, h);
varStore.set(`@test/${prefix}-${i}`, h);
hashes.push(h);
if (delayMs > 0 && i < n - 1) {
await new Promise((r) => setTimeout(r, delayMs));
@@ -43,7 +43,7 @@ async function setN(prefix: string, n: number, delayMs = 2): Promise<Hash[]> {
describe("VariableStore.list - pagination + sort", () => {
test("D1. default sort = created ASC", async () => {
await setN("v", 3);
const list = varStore.list({ namePrefix: "v-" });
const list = varStore.list({ namePrefix: "@test/v-" });
for (let i = 1; i < list.length; i++) {
expect((list[i] as { created: number }).created).toBeGreaterThanOrEqual(
(list[i - 1] as { created: number }).created,
@@ -56,53 +56,53 @@ describe("VariableStore.list - pagination + sort", () => {
await new Promise((r) => setTimeout(r, 5));
// Re-set u-0 with a NEW value so updated changes
const newHash = await casStore.put(stringHash, "u-0-new");
varStore.set("u-0", newHash);
varStore.set("@test/u-0", newHash);
const byUpdated = varStore.list({
namePrefix: "u-",
namePrefix: "@test/u-",
sort: "updated",
});
// u-0 should be last when sorted updated ASC
const last = byUpdated[byUpdated.length - 1] as { name: string };
expect(last.name).toBe("u-0");
expect(last.name).toBe("@test/u-0");
});
test("D3. desc reverses both sort modes", async () => {
await setN("d", 3);
const asc = varStore.list({ namePrefix: "d-" });
const desc = varStore.list({ namePrefix: "d-", desc: true });
const asc = varStore.list({ namePrefix: "@test/d-" });
const desc = varStore.list({ namePrefix: "@test/d-", desc: true });
expect(desc[0]).toEqual(asc[asc.length - 1] as (typeof asc)[number]);
});
test("D4. limit/offset honored", async () => {
await setN("p", 5);
expect(varStore.list({ namePrefix: "p-", limit: 2 })).toHaveLength(2);
expect(varStore.list({ namePrefix: "@test/p-", limit: 2 })).toHaveLength(2);
expect(
varStore.list({ namePrefix: "p-", offset: 2, limit: 10 }),
varStore.list({ namePrefix: "@test/p-", offset: 2, limit: 10 }),
).toHaveLength(3);
});
test("D5. core has no default limit (returns all)", async () => {
await setN("big", 105, 0);
const list = varStore.list({ namePrefix: "big-" });
const list = varStore.list({ namePrefix: "@test/big-" });
expect(list).toHaveLength(105);
});
test("D6. pagination applied AFTER namePrefix/schema filters", async () => {
await setN("filt", 5);
const list = varStore.list({
namePrefix: "filt-",
namePrefix: "@test/filt-",
schema: stringHash,
limit: 2,
});
expect(list).toHaveLength(2);
for (const v of list) {
expect((v as { name: string }).name.startsWith("filt-")).toBe(true);
expect((v as { name: string }).name.startsWith("@test/filt-")).toBe(true);
}
});
test("limit: 0 returns empty array", async () => {
await setN("z", 3, 0);
expect(varStore.list({ namePrefix: "z-", limit: 0 })).toEqual([]);
expect(varStore.list({ namePrefix: "@test/z-", limit: 0 })).toEqual([]);
});
});
+186 -186
View File
@@ -141,10 +141,10 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Action: set() for new variable
const variable = varStore.set("config", dataHash);
const variable = varStore.set("@test/config", dataHash);
// Assertions
expect(variable.name).toBe("config");
expect(variable.name).toBe("@test/config");
expect(variable.schema).toBe(schemaHash);
expect(variable.value).toBe(dataHash);
expect(variable.created).toBeGreaterThan(0);
@@ -153,7 +153,7 @@ describe("VariableStore - set() Upsert Method", () => {
expect(variable.labels).toEqual([]);
// Verify in database
const retrieved = varStore.get("config", schemaHash);
const retrieved = varStore.get("@test/config", schemaHash);
expect(retrieved).not.toBeNull();
expect((retrieved as Variable).value).toBe(dataHash);
@@ -174,23 +174,23 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create initial variable
const created = varStore.set("config", hash1);
const created = varStore.set("@test/config", hash1);
const createdTime = created.created;
await new Promise((resolve) => setTimeout(resolve, 10));
// Update via set()
const updated = varStore.set("config", hash2);
const updated = varStore.set("@test/config", hash2);
// Assertions
expect(updated.name).toBe("config");
expect(updated.name).toBe("@test/config");
expect(updated.schema).toBe(schemaHash);
expect(updated.value).toBe(hash2); // Updated value
expect(updated.created).toBe(createdTime); // Created time unchanged
expect(updated.updated).toBeGreaterThan(createdTime); // Updated time changed
// Verify in database
const retrieved = varStore.get("config", schemaHash);
const retrieved = varStore.get("@test/config", schemaHash);
expect((retrieved as Variable).value).toBe(hash2);
varStore.close();
@@ -205,7 +205,7 @@ describe("VariableStore - set() Upsert Method", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
const variable = varStore.set("config", dataHash, {
const variable = varStore.set("@test/config", dataHash, {
tags: { env: "prod", region: "us-east" },
labels: ["critical", "monitored"],
});
@@ -230,13 +230,13 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create with tags/labels
varStore.set("config", hash1, {
varStore.set("@test/config", hash1, {
tags: { env: "prod" },
labels: ["critical"],
});
// Update value only (no options)
const updated = varStore.set("config", hash2);
const updated = varStore.set("@test/config", hash2);
// Tags/labels should be preserved
expect(updated.value).toBe(hash2);
@@ -264,18 +264,18 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create two variables with same name, different schemas
const varA = varStore.set("config", hashA);
const varB = varStore.set("config", hashB);
const varA = varStore.set("@test/config", hashA);
const varB = varStore.set("@test/config", hashB);
expect(varA.name).toBe("config");
expect(varA.name).toBe("@test/config");
expect(varA.schema).toBe(schemaA);
expect(varB.name).toBe("config");
expect(varB.name).toBe("@test/config");
expect(varB.schema).toBe(schemaB);
expect(varA.value).not.toBe(varB.value);
// Verify both exist independently
expect((varStore.get("config", schemaA) as Variable).value).toBe(hashA);
expect((varStore.get("config", schemaB) as Variable).value).toBe(hashB);
expect((varStore.get("@test/config", schemaA) as Variable).value).toBe(hashA);
expect((varStore.get("@test/config", schemaB) as Variable).value).toBe(hashB);
varStore.close();
});
@@ -327,16 +327,16 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// When: set() with same name but different value schemas
const varA = varStore.set("config", valueA);
const varB = varStore.set("config", valueB);
const varA = varStore.set("@test/config", valueA);
const varB = varStore.set("@test/config", valueB);
// Then: Both variables created with correct extracted schemas
expect(varA.schema).toBe(schemaA);
expect(varB.schema).toBe(schemaB);
// Verify they coexist independently
const retrievedA = varStore.get("config", schemaA);
const retrievedB = varStore.get("config", schemaB);
const retrievedA = varStore.get("@test/config", schemaA);
const retrievedB = varStore.get("@test/config", schemaB);
expect((retrievedA as Variable).value).toBe(valueA);
expect((retrievedB as Variable).value).toBe(valueB);
@@ -354,10 +354,10 @@ describe("VariableStore - set() Upsert Method", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", value1);
varStore.set("@test/config", value1);
// When: set() with same name and same schema (extracted)
const updated = varStore.set("config", value2);
const updated = varStore.set("@test/config", value2);
// Then: Updates existing variable, not creates new
expect(updated.value).toBe(value2);
@@ -373,10 +373,10 @@ describe("VariableStore - set() Upsert Method", () => {
const fakeHash = "FAKEHASH00000";
expect(() => varStore.set("config", fakeHash)).toThrow(
expect(() => varStore.set("@test/config", fakeHash)).toThrow(
CasNodeNotFoundError,
);
expect(() => varStore.set("config", fakeHash)).toThrow(
expect(() => varStore.set("@test/config", fakeHash)).toThrow(
`CAS node not found: ${fakeHash}`,
);
@@ -395,11 +395,11 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create with tags
varStore.set("config", hash1, { tags: { env: "prod" } });
varStore.set("@test/config", hash1, { tags: { env: "prod" } });
// Try to update with conflicting tag/label
expect(() => {
varStore.set("config", hash2, {
varStore.set("@test/config", hash2, {
tags: { region: "us" },
labels: ["region"], // conflicts with tag key
});
@@ -420,11 +420,11 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create with labels
varStore.set("config", hash1, { labels: ["production"] });
varStore.set("@test/config", hash1, { labels: ["production"] });
// Try to update with conflicting label/tag
expect(() => {
varStore.set("config", hash2, {
varStore.set("@test/config", hash2, {
tags: { production: "true" }, // conflicts with existing label "production"
// labels not provided - existing ["production"] preserved, causing conflict
});
@@ -445,13 +445,13 @@ describe("VariableStore - set() Upsert Method", () => {
const varStore = new VariableStore(dbPath, store);
// Create with tags and labels
varStore.set("config", hash1, {
varStore.set("@test/config", hash1, {
tags: { env: "dev" },
labels: ["experimental"],
});
// Update with different tags/labels (no conflicts)
const updated = varStore.set("config", hash2, {
const updated = varStore.set("@test/config", hash2, {
tags: { region: "us", version: "2" },
labels: ["stable", "reviewed"],
});
@@ -486,14 +486,14 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", value);
varStore.set("@test/config", value);
// When: get() with exact (name, schema)
const result = varStore.get("config", schema);
const result = varStore.get("@test/config", schema);
// Then: Returns Variable object
expect(result).not.toBeNull();
expect((result as Variable).name).toBe("config");
expect((result as Variable).name).toBe("@test/config");
expect((result as Variable).schema).toBe(schema);
expect((result as Variable).value).toBe(value);
@@ -509,7 +509,7 @@ describe("VariableStore - get() with Optional Schema", () => {
const varStore = new VariableStore(dbPath, store);
// When: Query non-existent name
const result = varStore.get("nonexistent", schema);
const result = varStore.get("@test/nonexistent", schema);
// Then: Returns null
expect(result).toBeNull();
@@ -528,10 +528,10 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", value);
varStore.set("@test/config", value);
// When: Query with wrong schema
const result = varStore.get("config", schemaB);
const result = varStore.get("@test/config", schemaB);
// Then: Returns null (schema mismatch)
expect(result).toBeNull();
@@ -551,12 +551,12 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", valueA);
varStore.set("config", valueB);
varStore.set("@test/config", valueA);
varStore.set("@test/config", valueB);
// When: Query each schema explicitly
const resultA = varStore.get("config", schemaA);
const resultB = varStore.get("config", schemaB);
const resultA = varStore.get("@test/config", schemaA);
const resultB = varStore.get("@test/config", schemaB);
// Then: Returns correct variant for each schema
expect(resultA).not.toBeNull();
@@ -579,12 +579,12 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", value, {
varStore.set("@test/config", value, {
tags: { env: "prod" },
labels: ["critical"],
});
const result = varStore.get("config", schema);
const result = varStore.get("@test/config", schema);
expect(result).not.toBeNull();
expect((result as Variable).tags).toEqual({ env: "prod" });
@@ -610,11 +610,11 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashA);
varStore.set("config", hashB);
varStore.set("@test/config", hashA);
varStore.set("@test/config", hashB);
const resultA = varStore.get("config", schemaA);
const resultB = varStore.get("config", schemaB);
const resultA = varStore.get("@test/config", schemaA);
const resultB = varStore.get("@test/config", schemaB);
// Should return exact matches, not arrays
expect(resultA).not.toBeNull();
@@ -646,10 +646,10 @@ describe("VariableStore - get() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashA);
varStore.set("@test/config", hashA);
// Query with wrong schema
const result = varStore.get("config", schemaB);
const result = varStore.get("@test/config", schemaB);
expect(result).toBeNull();
@@ -686,11 +686,11 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashA);
varStore.set("config", hashB);
varStore.set("@test/config", hashA);
varStore.set("@test/config", hashB);
// Remove all variants
const deleted = varStore.remove("config");
const deleted = varStore.remove("@test/config");
// Should return array of 2 deleted variables
expect(Array.isArray(deleted)).toBe(true);
@@ -701,8 +701,8 @@ describe("VariableStore - remove() with Optional Schema", () => {
expect(deletedSchemas).toContain(schemaB);
// Verify both are gone
expect(varStore.get("config", schemaA)).toBeNull();
expect(varStore.get("config", schemaB)).toBeNull();
expect(varStore.get("@test/config", schemaA)).toBeNull();
expect(varStore.get("@test/config", schemaB)).toBeNull();
varStore.close();
});
@@ -712,7 +712,7 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
const deleted = varStore.remove("nonexistent");
const deleted = varStore.remove("@test/nonexistent");
expect(Array.isArray(deleted)).toBe(true);
expect(deleted.length).toBe(0);
@@ -737,22 +737,22 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", hashA);
varStore.set("config", hashB);
varStore.set("@test/config", hashA);
varStore.set("@test/config", hashB);
// Remove only schemaA variant
const deleted = varStore.remove("config", schemaA);
const deleted = varStore.remove("@test/config", schemaA);
// Should return single deleted Variable (not array)
expect(deleted).not.toBeNull();
expect(Array.isArray(deleted)).toBe(false);
expect((deleted as Variable).name).toBe("config");
expect((deleted as Variable).name).toBe("@test/config");
expect((deleted as Variable).schema).toBe(schemaA);
expect((deleted as Variable).value).toBe(hashA);
// Verify schemaA is gone but schemaB remains
expect(varStore.get("config", schemaA)).toBeNull();
expect(varStore.get("config", schemaB)).not.toBeNull();
expect(varStore.get("@test/config", schemaA)).toBeNull();
expect(varStore.get("@test/config", schemaB)).not.toBeNull();
varStore.close();
});
@@ -765,7 +765,7 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
expect(() => varStore.remove("nonexistent", schemaHash)).toThrow(
expect(() => varStore.remove("@test/nonexistent", schemaHash)).toThrow(
VariableNotFoundError,
);
@@ -781,13 +781,13 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", dataHash, {
varStore.set("@test/config", dataHash, {
tags: { env: "prod" },
labels: ["critical"],
});
// Remove variable
varStore.remove("config");
varStore.remove("@test/config");
// Verify tags/labels are also deleted
const db = (varStore as unknown as { db: unknown }).db as {
@@ -799,12 +799,12 @@ describe("VariableStore - remove() with Optional Schema", () => {
.prepare(
"SELECT * FROM variable_tags WHERE variable_name = ? AND variable_schema = ?",
)
.all("config", schemaHash);
.all("@test/config", schemaHash);
const labels = db
.prepare(
"SELECT * FROM variable_labels WHERE variable_name = ? AND variable_schema = ?",
)
.all("config", schemaHash);
.all("@test/config", schemaHash);
expect(tags).toHaveLength(0);
expect(labels).toHaveLength(0);
@@ -821,15 +821,15 @@ describe("VariableStore - remove() with Optional Schema", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", dataHash);
varStore.set("@test/config", dataHash);
// Remove with name only (no schema)
const deleted = varStore.remove("config");
const deleted = varStore.remove("@test/config");
// Should return array with 1 element
expect(Array.isArray(deleted)).toBe(true);
expect(deleted.length).toBe(1);
expect(deleted[0]?.name).toBe("config");
expect(deleted[0]?.name).toBe("@test/config");
varStore.close();
});
@@ -857,16 +857,16 @@ describe("VariableStore - Name Validation", () => {
const varStore = new VariableStore(dbPath, store);
// All these should succeed
expect(() => varStore.set("simple", dataHash)).not.toThrow();
expect(() => varStore.set("with_underscore", dataHash)).not.toThrow();
expect(() => varStore.set("with-dash", dataHash)).not.toThrow();
expect(() => varStore.set("with.dot", dataHash)).not.toThrow();
expect(() => varStore.set("number123", dataHash)).not.toThrow();
expect(() => varStore.set("path/to/var", dataHash)).not.toThrow();
expect(() => varStore.set("@test/simple", dataHash)).not.toThrow();
expect(() => varStore.set("@test/with_underscore", dataHash)).not.toThrow();
expect(() => varStore.set("@test/with-dash", dataHash)).not.toThrow();
expect(() => varStore.set("@test/with.dot", dataHash)).not.toThrow();
expect(() => varStore.set("@test/number123", dataHash)).not.toThrow();
expect(() => varStore.set("@test/path/to/var", dataHash)).not.toThrow();
expect(() =>
varStore.set("deeply/nested/path/to/var", dataHash),
varStore.set("@test/deeply/nested/path/to/var", dataHash),
).not.toThrow();
expect(() => varStore.set("uwf.thread.id_123", dataHash)).not.toThrow();
expect(() => varStore.set("@test/uwf.thread.id_123", dataHash)).not.toThrow();
varStore.close();
});
@@ -900,7 +900,7 @@ describe("VariableStore - Name Validation", () => {
InvalidVariableNameError,
);
expect(() => varStore.set("hello world", dataHash)).toThrow(
/invalid character/i,
/must follow @scope\/name|invalid character/i,
);
// Special characters
@@ -939,7 +939,7 @@ describe("VariableStore - Name Validation", () => {
expect(() => varStore.set("a//b", dataHash)).toThrow(
InvalidVariableNameError,
);
expect(() => varStore.set("a//b", dataHash)).toThrow(/empty segment/i);
expect(() => varStore.set("a//b", dataHash)).toThrow(/must follow @scope\/name|empty segment/i);
// Triple slash
expect(() => varStore.set("a///b", dataHash)).toThrow(
@@ -962,13 +962,13 @@ describe("VariableStore - Name Validation", () => {
expect(() => varStore.set("/abc", dataHash)).toThrow(
InvalidVariableNameError,
);
expect(() => varStore.set("/abc", dataHash)).toThrow(/leading slash/i);
expect(() => varStore.set("/abc", dataHash)).toThrow(/must follow @scope\/name|leading slash/i);
// Trailing slash
expect(() => varStore.set("abc/", dataHash)).toThrow(
InvalidVariableNameError,
);
expect(() => varStore.set("abc/", dataHash)).toThrow(/trailing slash/i);
expect(() => varStore.set("abc/", dataHash)).toThrow(/must follow @scope\/name|trailing slash/i);
// Both
expect(() => varStore.set("/abc/", dataHash)).toThrow(
@@ -1050,7 +1050,7 @@ describe("VariableStore - validateName() Error Messages", () => {
varStore = new VariableStore(dbPath, store);
try {
varStore.set("valid/segment/bad@segment/more", dataHash);
varStore.set("@test/valid/segment/bad@segment/more", dataHash);
throw new Error("Expected InvalidVariableNameError");
} catch (e) {
expect(e).toBeInstanceOf(InvalidVariableNameError);
@@ -1070,7 +1070,7 @@ describe("VariableStore - validateName() Error Messages", () => {
varStore = new VariableStore(dbPath, store);
try {
varStore.set("a//b", dataHash);
varStore.set("@test/a//b", dataHash);
throw new Error("Expected InvalidVariableNameError");
} catch (e) {
expect(e).toBeInstanceOf(InvalidVariableNameError);
@@ -1090,18 +1090,18 @@ describe("VariableStore - validateName() Error Messages", () => {
// Leading slash
try {
varStore.set("/abc", dataHash);
varStore.set("@test//foo", dataHash);
throw new Error("Expected InvalidVariableNameError");
} catch (e) {
expect(e).toBeInstanceOf(InvalidVariableNameError);
const error = e as InvalidVariableNameError;
expect(error.reason).toMatch(/leading|start|begins/i);
expect(error.reason).toMatch(/empty segment|consecutive|leading|start|begins/i);
expect(error.reason).not.toMatch(/trailing|end/i);
}
// Trailing slash
try {
varStore.set("abc/", dataHash);
varStore.set("@test/abc/", dataHash);
throw new Error("Expected InvalidVariableNameError");
} catch (e) {
expect(e).toBeInstanceOf(InvalidVariableNameError);
@@ -1121,11 +1121,11 @@ describe("VariableStore - validateName() Error Messages", () => {
varStore = new VariableStore(dbPath, store);
// All these should succeed
expect(() => varStore.set("app.config", dataHash)).not.toThrow();
expect(() => varStore.set("my_variable", dataHash)).not.toThrow();
expect(() => varStore.set("test-name", dataHash)).not.toThrow();
expect(() => varStore.set("path/to/config.json", dataHash)).not.toThrow();
expect(() => varStore.set("v1.2.3-alpha_001", dataHash)).not.toThrow();
expect(() => varStore.set("@test/app.config", dataHash)).not.toThrow();
expect(() => varStore.set("@test/my_variable", dataHash)).not.toThrow();
expect(() => varStore.set("@test/test-name", dataHash)).not.toThrow();
expect(() => varStore.set("@test/path/to/config.json", dataHash)).not.toThrow();
expect(() => varStore.set("@test/v1.2.3-alpha_001", dataHash)).not.toThrow();
});
});
@@ -1169,45 +1169,45 @@ describe("VariableStore - Integration Tests", () => {
const varStore = new VariableStore(dbPath, store);
// 1. Set initial config
const var1 = varStore.set("app/server", configHash1);
const var1 = varStore.set("@test/app/server", configHash1);
expect(var1.value).toBe(configHash1);
// 2. Set state with same name, different schema
const var2 = varStore.set("app/server", stateHash1);
const var2 = varStore.set("@test/app/server", stateHash1);
expect(var2.schema).toBe(schemaState);
// 3. List all variants with exactName
const result = varStore.list({ exactName: "app/server" });
const result = varStore.list({ exactName: "@test/app/server" });
expect(result.length).toBe(2);
// 4. Get with schema returns single variable
const config = varStore.get("app/server", schemaConfig);
const config = varStore.get("@test/app/server", schemaConfig);
expect(config).not.toBeNull();
expect((config as Variable).value).toBe(configHash1);
// 5. Update config via set
const updated = varStore.set("app/server", configHash2);
const updated = varStore.set("@test/app/server", configHash2);
expect(updated.value).toBe(configHash2);
// 6. Update state via set
varStore.set("app/server", stateHash2);
varStore.set("@test/app/server", stateHash2);
// 7. Remove specific schema
const deletedState = varStore.remove("app/server", schemaState);
const deletedState = varStore.remove("@test/app/server", schemaState);
expect((deletedState as Variable).schema).toBe(schemaState);
// 8. Verify only config remains
const remaining = varStore.list({ exactName: "app/server" });
const remaining = varStore.list({ exactName: "@test/app/server" });
expect(remaining.length).toBe(1);
expect(remaining[0]?.schema).toBe(schemaConfig);
// 9. Remove all remaining
const deletedAll = varStore.remove("app/server");
const deletedAll = varStore.remove("@test/app/server");
expect(Array.isArray(deletedAll)).toBe(true);
expect(deletedAll.length).toBe(1);
// 10. Verify all gone
expect(varStore.get("app/server", schemaConfig)).toBeNull();
expect(varStore.get("@test/app/server", schemaConfig)).toBeNull();
varStore.close();
});
@@ -1226,19 +1226,19 @@ describe("VariableStore - Integration Tests", () => {
const varStore = new VariableStore(dbPath, store);
// Initial set with tags
varStore.set("app/version", v1, {
varStore.set("@test/app/version", v1, {
tags: { env: "dev", region: "us" },
labels: ["beta"],
});
// Upsert without options preserves tags
const updated1 = varStore.set("app/version", v2);
const updated1 = varStore.set("@test/app/version", v2);
expect(updated1.value).toBe(v2);
expect(updated1.tags).toEqual({ env: "dev", region: "us" });
expect(updated1.labels).toEqual(["beta"]);
// Upsert with new tags replaces them
const updated2 = varStore.set("app/version", v2, {
const updated2 = varStore.set("@test/app/version", v2, {
tags: { env: "prod" },
labels: ["stable"],
});
@@ -1271,16 +1271,16 @@ describe("VariableStore - Legacy Update Method", () => {
const varStore = new VariableStore(dbPath, store);
// update() should fail when variable doesn't exist
expect(() => varStore.update("config", schemaHash, dataHash)).toThrow(
expect(() => varStore.update("@test/config", schemaHash, dataHash)).toThrow(
VariableNotFoundError,
);
// set() creates it
varStore.set("config", dataHash);
varStore.set("@test/config", dataHash);
// Now update() should work
const newHash = await store.put(schemaHash, {});
const updated = varStore.update("config", schemaHash, newHash);
const updated = varStore.update("@test/config", schemaHash, newHash);
expect(updated.value).toBe(newHash);
varStore.close();
@@ -1297,9 +1297,9 @@ describe("VariableStore - Legacy Update Method", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", dataA);
varStore.set("@test/config", dataA);
expect(() => varStore.update("config", schemaA, dataB)).toThrow(
expect(() => varStore.update("@test/config", schemaA, dataB)).toThrow(
SchemaMismatchError,
);
@@ -1329,13 +1329,13 @@ describe("VariableStore - List Operation", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("var1", data1);
varStore.set("var2", data2);
varStore.set("@test/var1", data1);
varStore.set("@test/var2", data2);
const vars = varStore.list();
expect(vars.length).toBe(2);
expect(vars.map((v) => v.name).sort()).toEqual(["var1", "var2"]);
expect(vars.map((v) => v.name).sort()).toEqual(["@test/var1", "@test/var2"]);
varStore.close();
});
@@ -1349,14 +1349,14 @@ describe("VariableStore - List Operation", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("app/config", data);
varStore.set("app/state", data);
varStore.set("sys/config", data);
varStore.set("@test/app/config", data);
varStore.set("@test/app/state", data);
varStore.set("@test/sys/config", data);
const vars = varStore.list({ namePrefix: "app/" });
const vars = varStore.list({ namePrefix: "@test/app/" });
expect(vars.length).toBe(2);
expect(vars.every((v) => v.name.startsWith("app/"))).toBe(true);
expect(vars.every((v) => v.name.startsWith("@test/app/"))).toBe(true);
varStore.close();
});
@@ -1388,21 +1388,21 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", valueA);
varStore.set("config", valueB);
varStore.set("config", valueC);
varStore.set("other", valueA); // Different name, same schema
varStore.set("@test/config", valueA);
varStore.set("@test/config", valueB);
varStore.set("@test/config", valueC);
varStore.set("@test/other", valueA); // Different name, same schema
// When: list with exactName
const results = varStore.list({ exactName: "config" });
const results = varStore.list({ exactName: "@test/config" });
// Then: Returns all 3 schema variants, not "other"
// Then: Returns all 3 schema variants, not "@test/other"
expect(results.length).toBe(3);
const schemas = results.map((v) => v.schema).sort();
expect(schemas).toContain(schemaA);
expect(schemas).toContain(schemaB);
expect(schemas).toContain(schemaC);
expect(results.every((v) => v.name === "config")).toBe(true);
expect(results.every((v) => v.name === "@test/config")).toBe(true);
varStore.close();
});
@@ -1414,7 +1414,7 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
const results = varStore.list({ exactName: "nonexistent" });
const results = varStore.list({ exactName: "@test/nonexistent" });
expect(results).toEqual([]);
varStore.close();
@@ -1432,11 +1432,11 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", valueA);
varStore.set("config", valueB);
varStore.set("@test/config", valueA);
varStore.set("@test/config", valueB);
// When: Filter by both exactName and schema
const results = varStore.list({ exactName: "config", schema: schemaA });
const results = varStore.list({ exactName: "@test/config", schema: schemaA });
// Then: Returns only schemaA variant
expect(results.length).toBe(1);
@@ -1456,12 +1456,12 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", valueA, { tags: { env: "dev" } });
varStore.set("config", valueB, { tags: { env: "prod" } });
varStore.set("@test/config", valueA, { tags: { env: "dev" } });
varStore.set("@test/config", valueB, { tags: { env: "prod" } });
// When: Filter by exactName + tags
const results = varStore.list({
exactName: "config",
exactName: "@test/config",
tags: { env: "prod" },
});
@@ -1481,7 +1481,7 @@ describe("VariableStore - list() with exactName", () => {
// When: Both provided
expect(() => {
varStore.list({ exactName: "config", namePrefix: "app/" });
varStore.list({ exactName: "@test/config", namePrefix: "app/" });
}).toThrow(/mutually exclusive|cannot specify both/i);
varStore.close();
@@ -1496,19 +1496,19 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("app", value);
varStore.set("app/config", value);
varStore.set("application", value);
varStore.set("@test/app", value);
varStore.set("@test/app/config", value);
varStore.set("@test/application", value);
// When: namePrefix without trailing slash
const results = varStore.list({ namePrefix: "app" });
const results = varStore.list({ namePrefix: "@test/app" });
// Then: Matches all three (prefix match)
expect(results.length).toBe(3);
expect(results.map((v) => v.name).sort()).toEqual([
"app",
"app/config",
"application",
"@test/app",
"@test/app/config",
"@test/application",
]);
varStore.close();
@@ -1528,15 +1528,15 @@ describe("VariableStore - list() with exactName", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", valueA);
varStore.set("config", valueB);
varStore.set("@test/config", valueA);
varStore.set("@test/config", valueB);
// Old way: get("config") → Variable | Variable[]
// New way: list({ exactName: "config" }) → Variable[]
const results = varStore.list({ exactName: "config" });
// Old way: get("@test/config") → Variable | Variable[]
// New way: list({ exactName: "@test/config" }) → Variable[]
const results = varStore.list({ exactName: "@test/config" });
expect(results.length).toBe(2);
expect(results.every((v) => v.name === "config")).toBe(true);
expect(results.every((v) => v.name === "@test/config")).toBe(true);
varStore.close();
});
@@ -1563,9 +1563,9 @@ describe("VariableStore - Tag/Label Management", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", dataHash);
varStore.set("@test/config", dataHash);
const updated = varStore.tag("config", schemaHash, {
const updated = varStore.tag("@test/config", schemaHash, {
add: { env: "prod", region: "us" },
});
@@ -1583,10 +1583,10 @@ describe("VariableStore - Tag/Label Management", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("config", dataHash, { labels: ["critical"] });
varStore.set("@test/config", dataHash, { labels: ["critical"] });
expect(() =>
varStore.tag("config", schemaHash, {
varStore.tag("@test/config", schemaHash, {
add: { critical: "yes" },
}),
).toThrow(TagLabelConflictError);
@@ -1644,10 +1644,10 @@ describe("VariableStore - @ Prefix Variable Names", () => {
const varStore = new VariableStore(dbPath, store);
// Single segment with @
varStore.set("@config", hash);
const result = varStore.get("@config", schemaHash);
varStore.set("@test/config", hash);
const result = varStore.get("@test/config", schemaHash);
expect(result).not.toBeNull();
expect(result?.name).toBe("@config");
expect(result?.name).toBe("@test/config");
varStore.close();
});
@@ -1665,8 +1665,8 @@ describe("VariableStore - @ Prefix Variable Names", () => {
const validNames = [
"@ocas/render/template",
"@system/config",
"@foo.bar/baz",
"@app-1/test_2",
"@foo/bar.baz",
"@app1/test_2",
];
for (const name of validNames) {
@@ -1737,12 +1737,12 @@ describe("VariableStore - @ Prefix Variable Names", () => {
// All non-@ names should continue to work
const validNames = [
"simple",
"with.dots",
"with-dashes",
"with_underscores",
"path/to/var",
"foo.bar/baz-qux/test_123",
"@test/simple",
"@test/with.dots",
"@test/with-dashes",
"@test/with_underscores",
"@test/path/to/var",
"@test/foo.bar/baz-qux/test_123",
];
for (const name of validNames) {
@@ -1802,9 +1802,9 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("x", v1);
varStore.set("@test/x", v1);
const hist = varStore.history("x", schema);
const hist = varStore.history("@test/x", schema);
expect(hist).toEqual([v1]);
varStore.close();
});
@@ -1819,11 +1819,11 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("x", v1);
varStore.set("x", v2);
varStore.set("x", v3);
varStore.set("@test/x", v1);
varStore.set("@test/x", v2);
varStore.set("@test/x", v3);
expect(varStore.history("x", schema)).toEqual([v3, v2, v1]);
expect(varStore.history("@test/x", schema)).toEqual([v3, v2, v1]);
varStore.close();
});
@@ -1835,13 +1835,13 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
const created = varStore.set("x", v1);
const created = varStore.set("@test/x", v1);
const updatedTime = created.updated;
await new Promise((r) => setTimeout(r, 5));
const second = varStore.set("x", v1);
const second = varStore.set("@test/x", v1);
expect(second.updated).toBe(updatedTime);
expect(varStore.history("x", schema)).toEqual([v1]);
expect(varStore.history("@test/x", schema)).toEqual([v1]);
varStore.close();
});
@@ -1855,13 +1855,13 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("x", v1);
varStore.set("x", v2);
varStore.set("x", v3);
varStore.set("@test/x", v1);
varStore.set("@test/x", v2);
varStore.set("@test/x", v3);
// History: [v3, v2, v1]; setting v1 should yield [v1, v3, v2]
varStore.set("x", v1);
varStore.set("@test/x", v1);
expect(varStore.history("x", schema)).toEqual([v1, v3, v2]);
expect(varStore.history("@test/x", schema)).toEqual([v1, v3, v2]);
varStore.close();
});
@@ -1877,10 +1877,10 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
for (const v of values) {
varStore.set("x", v);
varStore.set("@test/x", v);
}
const hist = varStore.history("x", schema);
const hist = varStore.history("@test/x", schema);
expect(hist).toHaveLength(MAX_HISTORY);
expect(hist).toEqual(values.slice(-MAX_HISTORY).reverse());
varStore.close();
@@ -1896,15 +1896,15 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("x", v1);
varStore.set("x", v2);
varStore.set("x", v3);
varStore.set("@test/x", v1);
varStore.set("@test/x", v2);
varStore.set("@test/x", v3);
// History: [v3, v2, v1]; rolling back is just calling set() with v1
const result = varStore.set("x", v1);
const result = varStore.set("@test/x", v1);
expect(result.value).toBe(v1);
expect(varStore.history("x", schema)).toEqual([v1, v3, v2]);
expect((varStore.get("x", schema) as Variable).value).toBe(v1);
expect(varStore.history("@test/x", schema)).toEqual([v1, v3, v2]);
expect((varStore.get("@test/x", schema) as Variable).value).toBe(v1);
varStore.close();
});
@@ -1917,12 +1917,12 @@ describe("VariableStore - History (LRU)", () => {
dbPath = tmpDbPath();
const varStore = new VariableStore(dbPath, store);
varStore.set("x", v1);
varStore.set("x", v2);
expect(varStore.history("x", schema)).toHaveLength(2);
varStore.set("@test/x", v1);
varStore.set("@test/x", v2);
expect(varStore.history("@test/x", schema)).toHaveLength(2);
varStore.remove("x", schema);
expect(varStore.history("x", schema)).toEqual([]);
varStore.remove("@test/x", schema);
expect(varStore.history("@test/x", schema)).toEqual([]);
varStore.close();
});
+17 -18
View File
@@ -136,49 +136,48 @@ export class VariableStore {
}
/**
* Validate variable name format
* @ is allowed at the start of the first segment (system-reserved)
* Validate variable name format.
* All names must follow @scope/name pattern:
* - scope: @[a-zA-Z][a-zA-Z0-9]* (e.g. @myapp, @ocas)
* - name: one or more segments of [a-zA-Z0-9._-]+ separated by /
* Examples: @myapp/config, @todo/schema, @ocas/schema
*/
private validateName(name: string): void {
// Rule 1: Cannot be empty
if (name === "") {
throw new InvalidVariableNameError(name, "Name cannot be empty");
}
// Rule 2: No leading slash
if (name.startsWith("/")) {
// Must match @scope/name where scope starts with a letter
const match = name.match(/^@([a-zA-Z][a-zA-Z0-9]*)\/(.+)$/);
if (!match) {
throw new InvalidVariableNameError(
name,
"Name cannot start with leading slash",
"Name must follow @scope/name format (e.g. @myapp/config)",
);
}
// Rule 3: No trailing slash
if (name.endsWith("/")) {
const rest = match[2] as string;
// Validate remaining segments
if (rest.endsWith("/")) {
throw new InvalidVariableNameError(
name,
"Name cannot end with trailing slash",
);
}
// Rule 4: Each segment must match [a-zA-Z0-9._-]+ (with @ allowed at start of first segment)
const segments = name.split("/");
for (let i = 0; i < segments.length; i++) {
const segment = segments[i] as string;
const segments = rest.split("/");
for (const segment of segments) {
if (segment === "") {
throw new InvalidVariableNameError(
name,
"Name contains empty segment (consecutive slashes //)",
);
}
// Check for invalid characters
// First segment can start with @, all segments can contain [a-zA-Z0-9._-]
const regex = i === 0 ? /^@?[a-zA-Z0-9._-]+$/ : /^[a-zA-Z0-9._-]+$/;
if (!regex.test(segment)) {
if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
throw new InvalidVariableNameError(
name,
`Segment "${segment}" contains invalid characters (only ${i === 0 ? "@, " : ""}a-z, A-Z, 0-9, ., _, - allowed)`,
`Segment "${segment}" contains invalid characters (only a-z, A-Z, 0-9, ., _, - allowed)`,
);
}
}