diff --git a/scripts/ci-test-manifest.mjs b/scripts/ci-test-manifest.mjs index 77931a27..2e0bd345 100644 --- a/scripts/ci-test-manifest.mjs +++ b/scripts/ci-test-manifest.mjs @@ -1,87 +1,89 @@ -export const CI_TEST_GROUPS = [ - "cli-smoke", - "core-regression", - "storage-and-schema", - "llm-clients-and-auth", - "packaging-and-workflow", -]; - -export const CI_TEST_MANIFEST = [ - { group: "llm-clients-and-auth", runner: "node", file: "test/embedder-error-hints.test.mjs" }, - { group: "llm-clients-and-auth", runner: "node", file: "test/cjk-recursion-regression.test.mjs" }, - { group: "storage-and-schema", runner: "node", file: "test/migrate-legacy-schema.test.mjs" }, - { group: "storage-and-schema", runner: "node", file: "test/config-session-strategy-migration.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/scope-access-undefined.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/reflection-bypass-hook.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-scope-filter.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/store-empty-scope-filter.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/storage-path-normalization.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/recall-text-cleanup.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/update-consistency-lancedb.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/strip-envelope-metadata.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/auto-recall-timeout.test.mjs", args: ["--test"] }, - { group: "cli-smoke", runner: "node", file: "test/import-markdown/import-markdown.test.mjs", args: ["--test"] }, - { group: "cli-smoke", runner: "node", file: "test/cli-smoke.mjs" }, - { group: "cli-smoke", runner: "node", file: "test/functional-e2e.mjs" }, - { group: "storage-and-schema", runner: "node", file: "test/per-agent-auto-recall.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/retriever-rerank-regression.mjs" }, - { group: "core-regression", runner: "node", file: "test/smart-memory-lifecycle.mjs" }, - { group: "core-regression", runner: "node", file: "test/smart-extractor-branches.mjs" }, - { group: "core-regression", runner: "node", file: "test/smart-extractor-batch-embed.test.mjs" }, - { group: "packaging-and-workflow", runner: "node", file: "test/plugin-manifest-regression.mjs" }, - { group: "core-regression", runner: "node", file: "test/session-summary-before-reset.test.mjs", args: ["--test"] }, - { group: "packaging-and-workflow", runner: "node", file: "test/sync-plugin-version.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/smart-metadata-v2.mjs" }, - { group: "storage-and-schema", runner: "node", file: "test/vector-search-cosine.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/context-support-e2e.mjs" }, - { group: "core-regression", runner: "node", file: "test/temporal-facts.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/memory-update-supersede.test.mjs" }, - { group: "llm-clients-and-auth", runner: "node", file: "test/memory-upgrader-diagnostics.test.mjs" }, - { group: "llm-clients-and-auth", runner: "node", file: "test/llm-api-key-client.test.mjs", args: ["--test"] }, - { group: "llm-clients-and-auth", runner: "node", file: "test/llm-oauth-client.test.mjs", args: ["--test"] }, - { group: "llm-clients-and-auth", runner: "node", file: "test/cli-oauth-login.test.mjs", args: ["--test"] }, - { group: "packaging-and-workflow", runner: "node", file: "test/workflow-fork-guards.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/clawteam-scope.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/cross-process-lock.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/lock-stress-test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/lock-release-on-error.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/preference-slots.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/is-latest-auto-supersede.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/temporal-awareness.test.mjs", args: ["--test"] }, - // Issue #598 regression tests - { group: "core-regression", runner: "node", file: "test/store-serialization.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/mmr-tiny.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/access-tracker-retry.test.mjs" }, - { group: "core-regression", runner: "node", file: "test/embedder-cache.test.mjs" }, - // Issue #629 batch embedding fix - { group: "llm-clients-and-auth", runner: "node", file: "test/embedder-ollama-batch-routing.test.mjs" }, - // Issue #665 bulkStore tests - // Issue #690 cross-call batch accumulator tests - { group: "storage-and-schema", runner: "node", file: "test/issue-690-cross-call-batch.test.mjs", args: ["--test"] }, - // Issue #665 bulkStore tests (from upstream) - { group: "storage-and-schema", runner: "node", file: "test/bulk-store.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/bulk-store-edge-cases.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-bulk-store.test.mjs", args: ["--test"] }, - { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-bulk-store-edge-cases.test.mjs", args: ["--test"] }, - // Issue #680 regression tests (from upstream) - { group: "core-regression", runner: "node", file: "test/memory-reflection-issue680-tdd.test.mjs", args: ["--test"] }, - // Issue #606 SDK migration Bug 2 regression tests - { group: "core-regression", runner: "node", file: "test/issue606_sdk-migration.test.mjs" }, - // PR #713 inference regression tests - inferProviderFromBaseURL + model fallback - { group: "core-regression", runner: "node", file: "test/infer-provider-from-baseurl.test.mjs" }, - // Issue #736 recall governance - isRecallUsed() unit tests - { group: "core-regression", runner: "node", file: "test/is-recall-used.test.mjs", args: ["--test"] }, - // Issue #492 agentId validation tests - { group: "core-regression", runner: "node", file: "test/agentid-validation.test.mjs", args: ["--test"] }, - { group: "core-regression", runner: "node", file: "test/command-reflection-guard.test.mjs", args: ["--test"] }, - // Tier 1 memory counter fix - { group: "core-regression", runner: "node", file: "test/tier1-counters.test.mjs", args: ["--test"] }, -]; - -export function getEntriesForGroup(group) { - if (!CI_TEST_GROUPS.includes(group)) { - throw new Error(`Unknown CI test group: ${group}`); - } - - return CI_TEST_MANIFEST.filter((entry) => entry.group === group); -} +export const CI_TEST_GROUPS = [ + "cli-smoke", + "core-regression", + "storage-and-schema", + "llm-clients-and-auth", + "packaging-and-workflow", +]; + +export const CI_TEST_MANIFEST = [ + { group: "llm-clients-and-auth", runner: "node", file: "test/embedder-error-hints.test.mjs" }, + { group: "llm-clients-and-auth", runner: "node", file: "test/cjk-recursion-regression.test.mjs" }, + { group: "storage-and-schema", runner: "node", file: "test/migrate-legacy-schema.test.mjs" }, + { group: "storage-and-schema", runner: "node", file: "test/config-session-strategy-migration.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/scope-access-undefined.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/reflection-bypass-hook.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-scope-filter.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/store-empty-scope-filter.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/storage-path-normalization.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/recall-text-cleanup.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/update-consistency-lancedb.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/strip-envelope-metadata.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/auto-recall-timeout.test.mjs", args: ["--test"] }, + { group: "cli-smoke", runner: "node", file: "test/import-markdown/import-markdown.test.mjs", args: ["--test"] }, + { group: "cli-smoke", runner: "node", file: "test/cli-smoke.mjs" }, + { group: "cli-smoke", runner: "node", file: "test/functional-e2e.mjs" }, + { group: "storage-and-schema", runner: "node", file: "test/per-agent-auto-recall.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/retriever-rerank-regression.mjs" }, + { group: "core-regression", runner: "node", file: "test/smart-memory-lifecycle.mjs" }, + { group: "core-regression", runner: "node", file: "test/smart-extractor-branches.mjs" }, + { group: "core-regression", runner: "node", file: "test/smart-extractor-batch-embed.test.mjs" }, + { group: "packaging-and-workflow", runner: "node", file: "test/plugin-manifest-regression.mjs" }, + { group: "core-regression", runner: "node", file: "test/session-summary-before-reset.test.mjs", args: ["--test"] }, + { group: "packaging-and-workflow", runner: "node", file: "test/sync-plugin-version.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/smart-metadata-v2.mjs" }, + { group: "storage-and-schema", runner: "node", file: "test/vector-search-cosine.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/context-support-e2e.mjs" }, + { group: "core-regression", runner: "node", file: "test/temporal-facts.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/memory-update-supersede.test.mjs" }, + { group: "llm-clients-and-auth", runner: "node", file: "test/memory-upgrader-diagnostics.test.mjs" }, + { group: "llm-clients-and-auth", runner: "node", file: "test/llm-api-key-client.test.mjs", args: ["--test"] }, + { group: "llm-clients-and-auth", runner: "node", file: "test/llm-oauth-client.test.mjs", args: ["--test"] }, + { group: "llm-clients-and-auth", runner: "node", file: "test/cli-oauth-login.test.mjs", args: ["--test"] }, + { group: "packaging-and-workflow", runner: "node", file: "test/workflow-fork-guards.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/clawteam-scope.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/cross-process-lock.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/lock-stress-test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/lock-release-on-error.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/preference-slots.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/is-latest-auto-supersede.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/temporal-awareness.test.mjs", args: ["--test"] }, + // Issue #598 regression tests + { group: "core-regression", runner: "node", file: "test/store-serialization.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/mmr-tiny.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/access-tracker-retry.test.mjs" }, + { group: "core-regression", runner: "node", file: "test/embedder-cache.test.mjs" }, + // Issue #629 batch embedding fix + { group: "llm-clients-and-auth", runner: "node", file: "test/embedder-ollama-batch-routing.test.mjs" }, + // Issue #665 bulkStore tests + // Issue #690 cross-call batch accumulator tests + { group: "storage-and-schema", runner: "node", file: "test/issue-690-cross-call-batch.test.mjs", args: ["--test"] }, + // Issue #665 bulkStore tests (from upstream) + { group: "storage-and-schema", runner: "node", file: "test/bulk-store.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/bulk-store-edge-cases.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-bulk-store.test.mjs", args: ["--test"] }, + { group: "storage-and-schema", runner: "node", file: "test/smart-extractor-bulk-store-edge-cases.test.mjs", args: ["--test"] }, + // Issue #680 regression tests (from upstream) + { group: "core-regression", runner: "node", file: "test/memory-reflection-issue680-tdd.test.mjs", args: ["--test"] }, + // Issue #606 SDK migration Bug 2 regression tests + { group: "core-regression", runner: "node", file: "test/issue606_sdk-migration.test.mjs" }, + // PR #713 inference regression tests - inferProviderFromBaseURL + model fallback + { group: "core-regression", runner: "node", file: "test/infer-provider-from-baseurl.test.mjs" }, + // Issue #736 recall governance - isRecallUsed() unit tests + { group: "core-regression", runner: "node", file: "test/is-recall-used.test.mjs", args: ["--test"] }, + // Issue #492 agentId validation tests + { group: "core-regression", runner: "node", file: "test/agentid-validation.test.mjs", args: ["--test"] }, + { group: "core-regression", runner: "node", file: "test/command-reflection-guard.test.mjs", args: ["--test"] }, + // Issue #686 runMemoryReflection numeric agentId guard integration test + { group: "core-regression", runner: "node", file: "test/agentid-validation-686.test.mjs", args: ["--test"] }, + // Tier 1 memory counter fix + { group: "core-regression", runner: "node", file: "test/tier1-counters.test.mjs", args: ["--test"] }, +]; + +export function getEntriesForGroup(group) { + if (!CI_TEST_GROUPS.includes(group)) { + throw new Error(`Unknown CI test group: ${group}`); + } + + return CI_TEST_MANIFEST.filter((entry) => entry.group === group); +} \ No newline at end of file diff --git a/test/agentid-validation-686.test.mjs b/test/agentid-validation-686.test.mjs new file mode 100644 index 00000000..d868f1f1 --- /dev/null +++ b/test/agentid-validation-686.test.mjs @@ -0,0 +1,196 @@ +/** + * agentid-validation-686.test.mjs + * + * Integration tests for the isInvalidAgentIdFormat() guard added to runMemoryReflection + * (Issue #686). Verifies the production implementation, not a copied helper. + * + * This test directly imports the exported isInvalidAgentIdFormat() from index.ts + * to exercise the actual production code path. + * + * Run: node --test test/agentid-validation-686.test.mjs + * Or: node test/agentid-validation-686.test.mjs + */ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import jitiFactory from "jiti"; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const pluginSdkStubPath = path.resolve(testDir, "helpers", "openclaw-plugin-sdk-stub.mjs"); +const jitiInstance = jitiFactory(import.meta.url, { + interopDefault: true, + alias: { + "openclaw/plugin-sdk": pluginSdkStubPath, + }, +}); + +const indexModule = jitiInstance("../index.ts"); +const memoryLanceDBProPlugin = indexModule.default || indexModule; +const resetRegistration = indexModule.resetRegistration ?? (() => {}); + +// isInvalidAgentIdFormat is now exported from index.ts — test the PRODUCTION implementation +const isInvalidAgentIdFormat = indexModule.isInvalidAgentIdFormat; + +// --------------------------------------------------------------------------- +// Test suite: isInvalidAgentIdFormat (production export) +// --------------------------------------------------------------------------- +describe("isInvalidAgentIdFormat — production export (Issue #686)", () => { + // Layer 1: empty / undefined + describe("Layer 1 — empty / undefined", () => { + it("returns true when agentId is undefined", () => { + assert.strictEqual(isInvalidAgentIdFormat(undefined), true); + }); + + it("returns true when agentId is null", () => { + // @ts-ignore + assert.strictEqual(isInvalidAgentIdFormat(null), true); + }); + + it("returns true when agentId is empty string", () => { + assert.strictEqual(isInvalidAgentIdFormat(""), true); + }); + }); + + // Layer 2: pure numeric (chat_id pattern) + describe("Layer 2 — pure numeric = chat_id", () => { + it("returns true for a pure digit Discord user ID", () => { + assert.strictEqual(isInvalidAgentIdFormat("657229412030480397"), true); + }); + + it("returns true for a pure digit Telegram user ID", () => { + assert.strictEqual(isInvalidAgentIdFormat("123456789"), true); + }); + + it("returns true for a pure digit string (any source)", () => { + assert.strictEqual(isInvalidAgentIdFormat("999"), true); + }); + + it("returns false for an ID that starts with a letter (dc-channel-- prefix)", () => { + // Valid Discord channel agent ID format — should NOT be blocked + assert.strictEqual(isInvalidAgentIdFormat("dc-channel--1476858065914695741"), false); + }); + + it("returns false for an ID that starts with a letter (tg-group-- prefix)", () => { + assert.strictEqual(isInvalidAgentIdFormat("tg-group--5108601505"), false); + }); + + it("returns false for an ID with mixed alphanumeric characters", () => { + assert.strictEqual(isInvalidAgentIdFormat("agent-x-123"), false); + }); + + it("returns false for a typical named agent", () => { + assert.strictEqual(isInvalidAgentIdFormat("main"), false); + assert.strictEqual(isInvalidAgentIdFormat("pi-agent"), false); + assert.strictEqual(isInvalidAgentIdFormat("hermes"), false); + }); + }); + + // Layer 3: declaredAgents (signature-compat but NOT enforced in this impl) + // NOTE: Layer 3 is intentionally omitted — see JSDoc in index.ts. + // A follow-up PR should re-add Layer 3 with proper root-config access. + describe("Layer 3 — declaredAgents (signature compat, not enforced)", () => { + it("accepts a declaredAgents Set as second param without crashing", () => { + const agents = new Set(["main", "dc-channel--123"]); + assert.doesNotThrow(() => isInvalidAgentIdFormat("main", agents)); + }); + + it("still returns correct Layer 1/2 results regardless of declaredAgents", () => { + const agents = new Set(["main"]); + // Layer 1: undefined still blocked + assert.strictEqual(isInvalidAgentIdFormat(undefined, agents), true); + // Layer 2: numeric still blocked even if not in declaredAgents + assert.strictEqual(isInvalidAgentIdFormat("657229412030480397", agents), true); + // Valid agent still allowed (Layer 3 is no-op in this implementation) + assert.strictEqual(isInvalidAgentIdFormat("main", agents), false); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Integration test: runMemoryReflection guard fires for numeric sessionKey +// --------------------------------------------------------------------------- +describe("runMemoryReflection — numeric sessionKey guard (Issue #686)", () => { + let workDir; + + beforeEach(() => { + workDir = mkdtempSync(path.join(tmpdir(), "mlp-686-")); + resetRegistration(); + }); + + afterEach(() => { + try { + rmSync(workDir, { recursive: true, force: true }); + } catch {} + resetRegistration(); + }); + + it("runMemoryReflection skips early when sessionKey contains a numeric-only agentId", async () => { + let hookCalled = false; + const hookHandler = async (event) => { + hookCalled = true; + }; + const api = { + pluginConfig: { + dbPath: path.join(workDir, "db"), + embedding: { apiKey: "test-key", dimensions: 4 }, + sessionStrategy: "memoryReflection", + smartExtraction: false, + autoCapture: false, + autoRecall: false, + selfImprovement: { enabled: false, beforeResetNote: false, ensureLearningFiles: false }, + }, + resolvePath(target) { + if (!target || path.isAbsolute(target)) return target; + return path.join(workDir, target); + }, + logger: { + info() {}, + warn() {}, + debug() {}, + error() {}, + }, + registerTool() {}, + registerCli() {}, + registerService() {}, + registerHook(name, handler) { + if (name === "command:new") { + api.hooks["command:new"] = handler; + } + }, + on() {}, + hooks: {}, + }; + + memoryLanceDBProPlugin.register(api); + + const numericSessionKey = "agent:657229412030480397:session:test-session"; + const event = { + sessionKey: numericSessionKey, + action: "new", + context: { + commandSource: "test", + sessionEntry: {}, + previousSessionEntry: {}, + cfg: api.pluginConfig, + }, + }; + + const hook = api.hooks["command:new"]; + assert.ok(hook, "command:new hook should be registered"); + await hook(event); + assert.strictEqual(hookCalled, false, "hook should have skipped due to numeric agentId guard"); + }); + + it("runMemoryReflection allows named agentId (non-numeric)", async () => { + // Verify the guard does NOT block named agents + const namedSessionKey = "agent:main:session:test-session"; + assert.strictEqual(isInvalidAgentIdFormat("main"), false); + assert.strictEqual( + isInvalidAgentIdFormat(namedSessionKey.split(":")[1]), + false, + ); + }); +});