Merge pull request 'chore: Phase 1 code style fixes and missing features' (#28) from fix/27-phase1-code-style-fixes into main
This commit was merged in pull request #28.
This commit is contained in:
@@ -62,7 +62,7 @@ const storePath =
|
||||
typeof flags.store === "string" ? flags.store : defaultStorePath;
|
||||
const compact = flags.json === true;
|
||||
|
||||
const defaultVarDbPath = join(defaultStorePath, "variables.db");
|
||||
const defaultVarDbPath = join(storePath, "variables.db");
|
||||
const varDbPath =
|
||||
typeof flags["var-db"] === "string" ? flags["var-db"] : defaultVarDbPath;
|
||||
|
||||
@@ -95,6 +95,46 @@ function openVarStore(): VariableStore {
|
||||
return createVariableStore(resolve(varDbPath), store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Variable schema's CAS hash
|
||||
* This is the type hash used in JSON envelopes
|
||||
*/
|
||||
async function getVariableSchemaHash(): Promise<Hash> {
|
||||
const store = openStore();
|
||||
|
||||
// Define the Variable JSON Schema (simple version for envelope)
|
||||
const variableSchema: JSONSchema = {
|
||||
title: "Variable",
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
scope: { type: "string" },
|
||||
value: { type: "string" },
|
||||
schema: { type: "string" },
|
||||
created: { type: "number" },
|
||||
updated: { type: "number" },
|
||||
},
|
||||
required: ["id", "scope", "value", "schema", "created", "updated"],
|
||||
};
|
||||
|
||||
// Compute hash or retrieve from store
|
||||
const hash = await putSchema(store, variableSchema);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap Variable output in JSON envelope
|
||||
*/
|
||||
async function wrapVariableEnvelope(
|
||||
variable: unknown,
|
||||
): Promise<{ type: Hash; value: unknown }> {
|
||||
const typeHash = await getVariableSchemaHash();
|
||||
return {
|
||||
type: typeHash,
|
||||
value: variable,
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Commands ----
|
||||
|
||||
async function cmdInit(): Promise<void> {
|
||||
@@ -276,7 +316,8 @@ async function cmdVarCreate(_args: string[]): Promise<void> {
|
||||
|
||||
try {
|
||||
const variable = varStore.create(scope, value);
|
||||
out(variable);
|
||||
const envelope = await wrapVariableEnvelope(variable);
|
||||
out(envelope);
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidScopeError || e instanceof CasNodeNotFoundError) {
|
||||
die(`Error: ${e.message}`);
|
||||
@@ -298,7 +339,8 @@ async function cmdVarGet(args: string[]): Promise<void> {
|
||||
if (variable === null) {
|
||||
die(`Error: Variable not found: ${id}`);
|
||||
}
|
||||
out(variable);
|
||||
const envelope = await wrapVariableEnvelope(variable);
|
||||
out(envelope);
|
||||
} finally {
|
||||
varStore.close();
|
||||
}
|
||||
@@ -316,7 +358,8 @@ async function cmdVarUpdate(args: string[]): Promise<void> {
|
||||
|
||||
try {
|
||||
const variable = varStore.update(id, value);
|
||||
out(variable);
|
||||
const envelope = await wrapVariableEnvelope(variable);
|
||||
out(envelope);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof VariableNotFoundError ||
|
||||
@@ -339,7 +382,8 @@ async function cmdVarDelete(args: string[]): Promise<void> {
|
||||
|
||||
try {
|
||||
const variable = varStore.delete(id);
|
||||
out(variable);
|
||||
const envelope = await wrapVariableEnvelope(variable);
|
||||
out(envelope);
|
||||
} catch (e) {
|
||||
if (e instanceof VariableNotFoundError) {
|
||||
die(`Error: ${e.message}`);
|
||||
@@ -350,6 +394,25 @@ async function cmdVarDelete(args: string[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdVarList(_args: string[]): Promise<void> {
|
||||
const scope = (flags.scope as string | undefined) ?? "";
|
||||
|
||||
const varStore = openVarStore();
|
||||
|
||||
try {
|
||||
const variables = varStore.list({ scope });
|
||||
const envelope = await wrapVariableEnvelope(variables);
|
||||
out(envelope);
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidScopeError) {
|
||||
die(`Error: ${e.message}`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
varStore.close();
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
console.log(`\
|
||||
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||
@@ -373,6 +436,7 @@ Commands:
|
||||
var get <id> Get a variable by ID
|
||||
var update <id> <hash> Update variable value
|
||||
var delete <id> Delete a variable
|
||||
var list [--scope <prefix>] List variables (optionally filter by scope prefix)
|
||||
|
||||
Flags:
|
||||
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
||||
@@ -466,6 +530,9 @@ switch (cmd) {
|
||||
case "delete":
|
||||
await cmdVarDelete(subRest);
|
||||
break;
|
||||
case "list":
|
||||
await cmdVarList(subRest);
|
||||
break;
|
||||
default:
|
||||
die(`Unknown var subcommand: ${sub ?? "(none)"}`);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ describe("CLI var commands", () => {
|
||||
let schemaHash: string;
|
||||
let hashA: string;
|
||||
let hashB: string;
|
||||
let testCounter = 0;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temporary paths
|
||||
storePath = join(tmpdir(), `test-cli-store-${Date.now()}`);
|
||||
// Create temporary paths with counter to ensure uniqueness
|
||||
testCounter++;
|
||||
storePath = join(tmpdir(), `test-cli-store-${Date.now()}-${testCounter}`);
|
||||
varDbPath = join(storePath, "variables.db");
|
||||
cliPath = join(import.meta.dir, "index.ts");
|
||||
|
||||
@@ -100,12 +102,18 @@ describe("CLI var commands", () => {
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output.id).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
|
||||
expect(output.scope).toBe("uwf/thread/");
|
||||
expect(output.value).toBe(hashA);
|
||||
expect(output.schema).toBe(schemaHash);
|
||||
expect(output.created).toBeGreaterThan(Date.now() - 5000);
|
||||
expect(output.updated).toBe(output.created);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
|
||||
// Check the actual variable in the value field
|
||||
const variable = output.value;
|
||||
expect(variable.id).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
|
||||
expect(variable.scope).toBe("uwf/thread/");
|
||||
expect(variable.value).toBe(hashA);
|
||||
expect(variable.schema).toBe(schemaHash);
|
||||
expect(variable.created).toBeGreaterThan(Date.now() - 5000);
|
||||
expect(variable.updated).toBe(variable.created);
|
||||
});
|
||||
|
||||
test("1.2: Create variable fails with scope not ending in /", () => {
|
||||
@@ -170,7 +178,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const created = JSON.parse(createResult.stdout);
|
||||
const created = JSON.parse(createResult.stdout).value;
|
||||
|
||||
// Get the variable
|
||||
const result = spawnSync(
|
||||
@@ -182,10 +190,16 @@ describe("CLI var commands", () => {
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output.id).toBe(created.id);
|
||||
expect(output.scope).toBe("uwf/thread/");
|
||||
expect(output.value).toBe(hashA);
|
||||
expect(output.schema).toBe(schemaHash);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
|
||||
// Check the actual variable in the value field
|
||||
const variable = output.value;
|
||||
expect(variable.id).toBe(created.id);
|
||||
expect(variable.scope).toBe("uwf/thread/");
|
||||
expect(variable.value).toBe(hashA);
|
||||
expect(variable.schema).toBe(schemaHash);
|
||||
});
|
||||
|
||||
test("2.2: Get non-existent variable", () => {
|
||||
@@ -219,7 +233,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const created = JSON.parse(createResult.stdout);
|
||||
const created = JSON.parse(createResult.stdout).value;
|
||||
|
||||
// Wait a bit to ensure different timestamp
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
@@ -234,10 +248,16 @@ describe("CLI var commands", () => {
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output.id).toBe(created.id);
|
||||
expect(output.value).toBe(hashB);
|
||||
expect(output.schema).toBe(schemaHash);
|
||||
expect(output.updated).toBeGreaterThan(created.created);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
|
||||
// Check the actual variable in the value field
|
||||
const variable = output.value;
|
||||
expect(variable.id).toBe(created.id);
|
||||
expect(variable.value).toBe(hashB);
|
||||
expect(variable.schema).toBe(schemaHash);
|
||||
expect(variable.updated).toBeGreaterThan(created.created);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,7 +307,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const created = JSON.parse(createResult.stdout);
|
||||
const created = JSON.parse(createResult.stdout).value;
|
||||
|
||||
// Try to update with different schema
|
||||
const result = spawnSync(
|
||||
@@ -319,7 +339,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const created = JSON.parse(createResult.stdout);
|
||||
const created = JSON.parse(createResult.stdout).value;
|
||||
|
||||
// Delete the variable
|
||||
const result = spawnSync(
|
||||
@@ -331,7 +351,13 @@ describe("CLI var commands", () => {
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output.id).toBe(created.id);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
|
||||
// Check the actual variable in the value field
|
||||
const variable = output.value;
|
||||
expect(variable.id).toBe(created.id);
|
||||
|
||||
// Verify it's deleted
|
||||
const getResult = spawnSync(
|
||||
@@ -355,6 +381,218 @@ describe("CLI var commands", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Group 6: Variable Listing", () => {
|
||||
test("6.1: List variables with scope prefix", () => {
|
||||
// Create variables with different scopes
|
||||
const createResult1 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"uwf/thread/",
|
||||
"--value",
|
||||
hashA,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult1.status).toBe(0);
|
||||
const var1 = JSON.parse(createResult1.stdout).value;
|
||||
|
||||
const createResult2 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"uwf/thread/",
|
||||
"--value",
|
||||
hashB,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult2.status).toBe(0);
|
||||
const var2 = JSON.parse(createResult2.stdout).value;
|
||||
|
||||
const createResult3 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"uwf/agent/",
|
||||
"--value",
|
||||
hashA,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult3.status).toBe(0);
|
||||
const var3 = JSON.parse(createResult3.stdout).value;
|
||||
|
||||
const createResult4 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"app/config/",
|
||||
"--value",
|
||||
hashA,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult4.status).toBe(0);
|
||||
|
||||
// List all variables with uwf/ prefix
|
||||
const listResult = spawnSync(
|
||||
"bun",
|
||||
[cliPath, "--store", storePath, "var", "list", "--scope", "uwf/"],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
|
||||
expect(listResult.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(listResult.stdout);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
expect(Array.isArray(output.value)).toBe(true);
|
||||
|
||||
// Check the actual variables in the value field
|
||||
const variables = output.value;
|
||||
expect(variables).toHaveLength(3);
|
||||
expect(
|
||||
variables.every((v: { scope: string }) => v.scope.startsWith("uwf/")),
|
||||
).toBe(true);
|
||||
|
||||
// Verify ordering by created timestamp
|
||||
expect(variables[0].id).toBe(var1.id);
|
||||
expect(variables[1].id).toBe(var2.id);
|
||||
expect(variables[2].id).toBe(var3.id);
|
||||
});
|
||||
|
||||
test("6.2: List all variables when no scope specified", () => {
|
||||
// Create variables with different scopes
|
||||
const createResult1 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"uwf/thread/",
|
||||
"--value",
|
||||
hashA,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult1.status).toBe(0);
|
||||
|
||||
const createResult2 = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"app/config/",
|
||||
"--value",
|
||||
hashB,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult2.status).toBe(0);
|
||||
|
||||
// List all variables without scope filter
|
||||
const listResult = spawnSync(
|
||||
"bun",
|
||||
[cliPath, "--store", storePath, "var", "list"],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
|
||||
expect(listResult.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(listResult.stdout);
|
||||
// Expect envelope format
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
expect(Array.isArray(output.value)).toBe(true);
|
||||
|
||||
const variables = output.value;
|
||||
expect(variables).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("6.3: List returns empty array when no matches", () => {
|
||||
// Create a variable
|
||||
const createResult = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"create",
|
||||
"--scope",
|
||||
"uwf/thread/",
|
||||
"--value",
|
||||
hashA,
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult.status).toBe(0);
|
||||
|
||||
// List with non-matching scope
|
||||
const listResult = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
cliPath,
|
||||
"--store",
|
||||
storePath,
|
||||
"var",
|
||||
"list",
|
||||
"--scope",
|
||||
"nonexistent/",
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
|
||||
expect(listResult.status).toBe(0);
|
||||
|
||||
const output = JSON.parse(listResult.stdout);
|
||||
expect(output.type).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
expect(output.value).toBeDefined();
|
||||
expect(Array.isArray(output.value)).toBe(true);
|
||||
expect(output.value).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("6.4: List fails with invalid scope format", () => {
|
||||
const listResult = spawnSync(
|
||||
"bun",
|
||||
[cliPath, "--store", storePath, "var", "list", "--scope", "uwf"],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
|
||||
expect(listResult.status).not.toBe(0);
|
||||
expect(listResult.stderr).toContain("scope must end with /");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Group 7: Integration Tests", () => {
|
||||
test("7.1: Full lifecycle workflow", async () => {
|
||||
// Create variable
|
||||
@@ -374,7 +612,7 @@ describe("CLI var commands", () => {
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(createResult.status).toBe(0);
|
||||
const var1 = JSON.parse(createResult.stdout);
|
||||
const var1 = JSON.parse(createResult.stdout).value;
|
||||
expect(var1.value).toBe(hashA);
|
||||
|
||||
// Get variable
|
||||
@@ -384,7 +622,7 @@ describe("CLI var commands", () => {
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(getResult1.status).toBe(0);
|
||||
const retrieved1 = JSON.parse(getResult1.stdout);
|
||||
const retrieved1 = JSON.parse(getResult1.stdout).value;
|
||||
expect(retrieved1.value).toBe(hashA);
|
||||
|
||||
// Wait to ensure different timestamp
|
||||
@@ -397,7 +635,7 @@ describe("CLI var commands", () => {
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(updateResult.status).toBe(0);
|
||||
const updated = JSON.parse(updateResult.stdout);
|
||||
const updated = JSON.parse(updateResult.stdout).value;
|
||||
expect(updated.value).toBe(hashB);
|
||||
|
||||
// Get updated variable
|
||||
@@ -407,7 +645,7 @@ describe("CLI var commands", () => {
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(getResult2.status).toBe(0);
|
||||
const retrieved2 = JSON.parse(getResult2.stdout);
|
||||
const retrieved2 = JSON.parse(getResult2.stdout).value;
|
||||
expect(retrieved2.value).toBe(hashB);
|
||||
|
||||
// Delete variable
|
||||
@@ -444,7 +682,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const var1 = JSON.parse(createResult1.stdout);
|
||||
const var1 = JSON.parse(createResult1.stdout).value;
|
||||
|
||||
const createResult2 = spawnSync(
|
||||
"bun",
|
||||
@@ -461,7 +699,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const var2 = JSON.parse(createResult2.stdout);
|
||||
const var2 = JSON.parse(createResult2.stdout).value;
|
||||
|
||||
// Verify independence
|
||||
expect(var1.id).not.toBe(var2.id);
|
||||
@@ -471,7 +709,7 @@ describe("CLI var commands", () => {
|
||||
[cliPath, "--store", storePath, "var", "get", var1.id],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const retrieved1 = JSON.parse(getResult1.stdout);
|
||||
const retrieved1 = JSON.parse(getResult1.stdout).value;
|
||||
expect(retrieved1.value).toBe(hashA);
|
||||
|
||||
const getResult2 = spawnSync(
|
||||
@@ -479,7 +717,7 @@ describe("CLI var commands", () => {
|
||||
[cliPath, "--store", storePath, "var", "get", var2.id],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const retrieved2 = JSON.parse(getResult2.stdout);
|
||||
const retrieved2 = JSON.parse(getResult2.stdout).value;
|
||||
expect(retrieved2.value).toBe(hashB);
|
||||
|
||||
// Delete var1, verify var2 still exists
|
||||
@@ -498,7 +736,7 @@ describe("CLI var commands", () => {
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
expect(getResult2Final.status).toBe(0);
|
||||
const retrieved2Final = JSON.parse(getResult2Final.stdout);
|
||||
const retrieved2Final = JSON.parse(getResult2Final.stdout).value;
|
||||
expect(retrieved2Final.value).toBe(hashB);
|
||||
});
|
||||
|
||||
@@ -518,7 +756,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const var1 = JSON.parse(createResult1.stdout);
|
||||
const var1 = JSON.parse(createResult1.stdout).value;
|
||||
|
||||
const createResult2 = spawnSync(
|
||||
"bun",
|
||||
@@ -535,7 +773,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const var2 = JSON.parse(createResult2.stdout);
|
||||
const var2 = JSON.parse(createResult2.stdout).value;
|
||||
|
||||
const createResult3 = spawnSync(
|
||||
"bun",
|
||||
@@ -552,7 +790,7 @@ describe("CLI var commands", () => {
|
||||
],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const var3 = JSON.parse(createResult3.stdout);
|
||||
const var3 = JSON.parse(createResult3.stdout).value;
|
||||
|
||||
expect(var1.scope).toBe("uwf/");
|
||||
expect(var2.scope).toBe("uwf/thread/");
|
||||
|
||||
@@ -235,6 +235,99 @@ describe("VariableStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Group 6: Variable Listing", () => {
|
||||
test("6.1: list() returns all variables with matching scope prefix", async () => {
|
||||
const var1 = varStore.create("uwf/thread/", hashA);
|
||||
const var2 = varStore.create("uwf/thread/", hashB);
|
||||
const var3 = varStore.create("uwf/agent/", hashA);
|
||||
varStore.create("app/config/", hashA);
|
||||
|
||||
// Wait a bit to ensure different timestamps
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const results = varStore.list({ scope: "uwf/" });
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results.every((v) => v.scope.startsWith("uwf/"))).toBe(true);
|
||||
|
||||
// Verify ordering by created timestamp
|
||||
expect(results[0]?.id).toBe(var1.id);
|
||||
expect(results[1]?.id).toBe(var2.id);
|
||||
expect(results[2]?.id).toBe(var3.id);
|
||||
});
|
||||
|
||||
test("6.2: list() returns empty array when no matches", () => {
|
||||
varStore.create("uwf/thread/", hashA);
|
||||
|
||||
const results = varStore.list({ scope: "nonexistent/" });
|
||||
|
||||
expect(results).toHaveLength(0);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
test("6.3: list() returns all variables when scope is empty string", () => {
|
||||
const var1 = varStore.create("uwf/thread/", hashA);
|
||||
const var2 = varStore.create("app/config/", hashB);
|
||||
const var3 = varStore.create("test/", hashC);
|
||||
|
||||
const results = varStore.list({ scope: "" });
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
expect(results.map((v) => v.id)).toContain(var1.id);
|
||||
expect(results.map((v) => v.id)).toContain(var2.id);
|
||||
expect(results.map((v) => v.id)).toContain(var3.id);
|
||||
});
|
||||
|
||||
test("6.4: list() validates scope format (must end with /)", () => {
|
||||
varStore.create("uwf/thread/", hashA);
|
||||
|
||||
expect(() => varStore.list({ scope: "uwf" })).toThrow(InvalidScopeError);
|
||||
expect(() => varStore.list({ scope: "uwf" })).toThrow(
|
||||
"scope must end with /",
|
||||
);
|
||||
});
|
||||
|
||||
test("6.5: list() returns exact scope match and sub-scopes", () => {
|
||||
varStore.create("uwf/thread/", hashA);
|
||||
varStore.create("uwf/thread/active/", hashB);
|
||||
|
||||
const results = varStore.list({ scope: "uwf/thread/" });
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]?.scope).toBe("uwf/thread/");
|
||||
expect(results[1]?.scope).toBe("uwf/thread/active/");
|
||||
});
|
||||
|
||||
test("6.6: list() result ordering is deterministic", async () => {
|
||||
// Create 5 variables with the same scope prefix
|
||||
const var1 = varStore.create("test/", hashA);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
const var2 = varStore.create("test/", hashB);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
const var3 = varStore.create("test/", hashA);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
const var4 = varStore.create("test/", hashB);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
const var5 = varStore.create("test/", hashA);
|
||||
|
||||
// Call list multiple times
|
||||
const results1 = varStore.list({ scope: "test/" });
|
||||
const results2 = varStore.list({ scope: "test/" });
|
||||
const results3 = varStore.list({ scope: "test/" });
|
||||
|
||||
// All results should be identical
|
||||
expect(results1.map((v) => v.id)).toEqual(results2.map((v) => v.id));
|
||||
expect(results2.map((v) => v.id)).toEqual(results3.map((v) => v.id));
|
||||
|
||||
// Verify ordering by created timestamp (oldest first)
|
||||
expect(results1[0]?.id).toBe(var1.id);
|
||||
expect(results1[1]?.id).toBe(var2.id);
|
||||
expect(results1[2]?.id).toBe(var3.id);
|
||||
expect(results1[3]?.id).toBe(var4.id);
|
||||
expect(results1[4]?.id).toBe(var5.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Group 7: Integration Tests", () => {
|
||||
test("7.1: Full lifecycle workflow", async () => {
|
||||
// Create variable
|
||||
|
||||
@@ -200,6 +200,43 @@ export class VariableStore {
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* List variables matching a scope prefix
|
||||
*/
|
||||
list(options?: { scope?: string }): Variable[] {
|
||||
const scope = options?.scope ?? "";
|
||||
|
||||
// Validate scope format (must end with / if non-empty)
|
||||
if (scope !== "" && !scope.endsWith("/")) {
|
||||
throw new InvalidScopeError(scope);
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, scope, value, schema, created, updated
|
||||
FROM variables
|
||||
WHERE scope LIKE ? || '%'
|
||||
ORDER BY created ASC
|
||||
`);
|
||||
|
||||
const rows = stmt.all(scope) as Array<{
|
||||
id: string;
|
||||
scope: string;
|
||||
value: string;
|
||||
schema: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
}>;
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
scope: row.scope,
|
||||
value: row.value,
|
||||
schema: row.schema,
|
||||
created: row.created,
|
||||
updated: row.updated,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
|
||||
@@ -8,11 +8,11 @@ export type VariableId = string;
|
||||
/**
|
||||
* Variable: mutable binding to an immutable CAS node
|
||||
*/
|
||||
export interface Variable {
|
||||
export type Variable = {
|
||||
id: VariableId;
|
||||
scope: string; // hierarchical path, must end with /
|
||||
value: Hash; // CAS node hash
|
||||
schema: Hash; // extracted from value's CAS node.type
|
||||
created: number; // epoch ms
|
||||
updated: number; // epoch ms
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user