From bfaa2722fc46d0b4958ee7dff309e0e13d50c7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 1 Jun 2026 16:12:23 +0000 Subject: [PATCH] feat: enforce @scope/name format for all variable names All variable names must now follow @scope/name pattern: - scope: @[a-zA-Z][a-zA-Z0-9]* (e.g. @myapp, @todo) - name: one or more segments of [a-zA-Z0-9._-]+ - @ocas/* reserved for internal use Examples: @myapp/config, @todo/schema, @ocas/schema (builtin) Rejected: config (no scope), foo/bar (no @), @/foo (empty scope) Updated validateName() in core, CLI error messages, and all tests. 594 tests pass. Fixes #29 --- packages/cli/src/index.ts | 8 +- .../__snapshots__/edge-cases.test.ts.snap | 24 +- packages/cli/tests/edge-cases.test.ts | 28 +- packages/cli/tests/gc.test.ts | 2 +- packages/cli/tests/list-pagination.test.ts | 22 +- packages/cli/tests/variable-history.test.ts | 20 +- packages/cli/tests/variable.test.ts | 172 ++++---- packages/core/src/gc.test.ts | 24 +- .../core/src/variable-list-pagination.test.ts | 26 +- packages/core/src/variable-store.test.ts | 372 +++++++++--------- packages/core/src/variable-store.ts | 35 +- 11 files changed, 368 insertions(+), 365 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c3a9f24..1409f60 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -621,7 +621,9 @@ async function cmdVarSet(args: string[]): Promise { } 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 { } 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(); diff --git a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap index 809db84..373127d 100644 --- a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap +++ b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap @@ -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 [--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 ] [--json] [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`] = ` { diff --git a/packages/cli/tests/edge-cases.test.ts b/packages/cli/tests/edge-cases.test.ts index ff99563..638f86e 100644 --- a/packages/cli/tests/edge-cases.test.ts +++ b/packages/cli/tests/edge-cases.test.ts @@ -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, ]); diff --git a/packages/cli/tests/gc.test.ts b/packages/cli/tests/gc.test.ts index eb3e0bb..b4d1302 100644 --- a/packages/cli/tests/gc.test.ts +++ b/packages/cli/tests/gc.test.ts @@ -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(() => { diff --git a/packages/cli/tests/list-pagination.test.ts b/packages/cli/tests/list-pagination.test.ts index a6e983d..5ad9dd6 100644 --- a/packages/cli/tests/list-pagination.test.ts +++ b/packages/cli/tests/list-pagination.test.ts @@ -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 () => { diff --git a/packages/cli/tests/variable-history.test.ts b/packages/cli/tests/variable-history.test.ts index c71b90d..07daeea 100644 --- a/packages/cli/tests/variable-history.test.ts +++ b/packages/cli/tests/variable-history.test.ts @@ -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]); diff --git a/packages/cli/tests/variable.test.ts b/packages/cli/tests/variable.test.ts index 54fd6c6..a16ae94 100644 --- a/packages/cli/tests/variable.test.ts +++ b/packages/cli/tests/variable.test.ts @@ -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, ], { diff --git a/packages/core/src/gc.test.ts b/packages/core/src/gc.test.ts index f9686bc..eb8d80f 100644 --- a/packages/core/src/gc.test.ts +++ b/packages/core/src/gc.test.ts @@ -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); diff --git a/packages/core/src/variable-list-pagination.test.ts b/packages/core/src/variable-list-pagination.test.ts index 3f4b8e4..93775c7 100644 --- a/packages/core/src/variable-list-pagination.test.ts +++ b/packages/core/src/variable-list-pagination.test.ts @@ -31,7 +31,7 @@ async function setN(prefix: string, n: number, delayMs = 2): Promise { 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 { 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([]); }); }); diff --git a/packages/core/src/variable-store.test.ts b/packages/core/src/variable-store.test.ts index cc9ace9..270fe0f 100644 --- a/packages/core/src/variable-store.test.ts +++ b/packages/core/src/variable-store.test.ts @@ -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(); }); diff --git a/packages/core/src/variable-store.ts b/packages/core/src/variable-store.ts index 240ec93..23d393d 100644 --- a/packages/core/src/variable-store.ts +++ b/packages/core/src/variable-store.ts @@ -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)`, ); } }