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
This commit is contained in:
@@ -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`] = `
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
{
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user