From 4796e4fe82194b10eebe0b8f4f9c8780440d3cef Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 16 Mar 2025 08:02:29 +0100 Subject: [PATCH] rrefactor --- aiprompts/starter/1_heroscript.md | 78 +++++++ ... instructions.md => 3_heroscript_vlang.md} | 42 ---- lib/circles/actionprocessor/factory.v | 18 +- lib/circles/actions/db/job_db.v | 109 +++++++++ lib/circles/actions/db/job_db_test.v | 211 +++++++++++++++++ lib/circles/actions/models/job.v | 218 ++++++++++++++++++ lib/circles/actions/models/job_test.v | 206 +++++++++++++++++ lib/circles/{models => base}/dbhandler.v | 9 +- lib/circles/{models => base}/sessionstate.v | 0 lib/circles/{dbs/core => core/db}/agent_db.v | 2 +- .../{dbs/core => core/db}/agent_db_test.v | 3 +- lib/circles/{dbs/core => core/db}/circle_db.v | 2 +- .../{dbs/core => core/db}/circle_db_test.v | 2 +- lib/circles/{dbs/core => core/db}/name_db.v | 2 +- .../{dbs/core => core/db}/name_db_test.v | 2 +- .../{models/core => core/models}/README.md | 0 .../{models/core => core/models}/agent.v | 0 .../{models/core => core/models}/agent_test.v | 0 .../{models/core => core/models}/circle.v | 0 .../core => core/models}/circle_test.v | 0 .../{models/core => core/models}/name.v | 0 .../{models/core => core/models}/name_test.v | 0 lib/circles/{dbs/mcc => mcc/db}/calendar_db.v | 2 +- .../{dbs/mcc => mcc/db}/calendar_db_test.v | 2 +- lib/circles/{dbs/mcc => mcc/db}/mail_db.v | 2 +- .../{dbs/mcc => mcc/db}/mail_db_test.v | 2 +- .../{models/mcc => mcc/models}/caledar.v | 0 .../mcc => mcc/models}/calendar_test.v | 0 lib/circles/{models/mcc => mcc/models}/mail.v | 0 .../mcc => mcc/models}/mail_simple_test.v | 0 .../{models/mcc => mcc/models}/mail_test.v | 0 lib/circles/models/jobs/job.v | 52 ----- lib/data/encoder/auto.v | 4 + lib/data/encoder/encoder_decode.v | 5 + lib/data/encoder/encoder_encode.v | 8 + lib/data/encoder/encoder_test.v | 27 ++- lib/data/encoder/readme.md | 3 + 37 files changed, 896 insertions(+), 115 deletions(-) create mode 100644 aiprompts/starter/1_heroscript.md rename aiprompts/starter/{3_heroscript & params instructions.md => 3_heroscript_vlang.md} (90%) create mode 100644 lib/circles/actions/db/job_db.v create mode 100644 lib/circles/actions/db/job_db_test.v create mode 100644 lib/circles/actions/models/job.v create mode 100644 lib/circles/actions/models/job_test.v rename lib/circles/{models => base}/dbhandler.v (94%) rename lib/circles/{models => base}/sessionstate.v (100%) rename lib/circles/{dbs/core => core/db}/agent_db.v (97%) rename lib/circles/{dbs/core => core/db}/agent_db_test.v (99%) rename lib/circles/{dbs/core => core/db}/circle_db.v (98%) rename lib/circles/{dbs/core => core/db}/circle_db_test.v (99%) rename lib/circles/{dbs/core => core/db}/name_db.v (98%) rename lib/circles/{dbs/core => core/db}/name_db_test.v (99%) rename lib/circles/{models/core => core/models}/README.md (100%) rename lib/circles/{models/core => core/models}/agent.v (100%) rename lib/circles/{models/core => core/models}/agent_test.v (100%) rename lib/circles/{models/core => core/models}/circle.v (100%) rename lib/circles/{models/core => core/models}/circle_test.v (100%) rename lib/circles/{models/core => core/models}/name.v (100%) rename lib/circles/{models/core => core/models}/name_test.v (100%) rename lib/circles/{dbs/mcc => mcc/db}/calendar_db.v (98%) rename lib/circles/{dbs/mcc => mcc/db}/calendar_db_test.v (98%) rename lib/circles/{dbs/mcc => mcc/db}/mail_db.v (98%) rename lib/circles/{dbs/mcc => mcc/db}/mail_db_test.v (99%) rename lib/circles/{models/mcc => mcc/models}/caledar.v (100%) rename lib/circles/{models/mcc => mcc/models}/calendar_test.v (100%) rename lib/circles/{models/mcc => mcc/models}/mail.v (100%) rename lib/circles/{models/mcc => mcc/models}/mail_simple_test.v (100%) rename lib/circles/{models/mcc => mcc/models}/mail_test.v (100%) delete mode 100644 lib/circles/models/jobs/job.v diff --git a/aiprompts/starter/1_heroscript.md b/aiprompts/starter/1_heroscript.md new file mode 100644 index 00000000..fa6fd90c --- /dev/null +++ b/aiprompts/starter/1_heroscript.md @@ -0,0 +1,78 @@ +# HeroScript + +## Overview + +HeroScript is a simple, declarative scripting language designed to define workflows and execute commands in a structured manner. It follows a straightforward syntax where each action is prefixed with `!!`, indicating the actor and action name. + +## Example + +A basic HeroScript script for virtual machine management looks like this: + +```heroscript +!!vm.define name:'test_vm' cpu:4 + memory: '8GB' + storage: '100GB' + description: ' + A virtual machine configuration + with specific resources. + ' + +!!vm.start name:'test_vm' + +!!vm.disk_add + name: 'test_vm' + size: '50GB' + type: 'SSD' + +!!vm.delete + name: 'test_vm' + force: true +``` + +### Key Features + +- Every action starts with `!!`. + - The first part after `!!` is the actor (e.g., `vm`). + - The second part is the action name (e.g., `define`, `start`, `delete`). +- Multi-line values are supported (e.g., the `description` field). +- Lists are comma-separated where applicable and inside ''. +- If items one 1 line, then no space between name & argument e.g. name:'test_vm' + +## Parsing HeroScript + +Internally, HeroScript gets parsed into an action object with parameters. Each parameter follows a `key: value` format. + +### Parsing Example + +```heroscript +!!actor.action + id:a1 name6:aaaaa + name:'need to do something 1' + description: + ' + ## markdown works in it + description can be multiline + lets see what happens + + - a + - something else + + ### subtitle + ' + + name2: test + name3: hi + name10:'this is with space' name11:aaa11 + + name4: 'aaa' + + //somecomment + name5: 'aab' +``` + +### Parsing Details +- Each parameter follows a `key: value` format. +- Multi-line values (such as descriptions) support Markdown formatting. +- Comments can be added using `//`. +- Keys and values can have spaces, and values can be enclosed in single quotes. + diff --git a/aiprompts/starter/3_heroscript & params instructions.md b/aiprompts/starter/3_heroscript_vlang.md similarity index 90% rename from aiprompts/starter/3_heroscript & params instructions.md rename to aiprompts/starter/3_heroscript_vlang.md index fea8cefe..2e7935e8 100644 --- a/aiprompts/starter/3_heroscript & params instructions.md +++ b/aiprompts/starter/3_heroscript_vlang.md @@ -1,45 +1,3 @@ -# how to work with heroscript in vlang - -## heroscript - -Heroscript is our small scripting language which has following structure - -an example of a heroscript is - -```heroscript - -!!dagu.script_define - name: 'test_dag' - homedir:'' - title:'a title' - reset:1 - start:true //trie or 1 is same - colors: 'green,red,purple' //lists are comma separated - description: ' - a description can be multiline - - like this - ' - - -!!dagu.add_step - dag: 'test_dag' - name: 'hello_world' - command: 'echo hello world' - -!!dagu.add_step - dag: 'test_dag' - name: 'last_step' - command: 'echo last step' - - -``` - -Notice how: -- every action starts with !! - - the first part is the actor e.g. dagu in this case - - the 2e part is the action name -- multilines are supported see the description field ## how to process heroscript in Vlang diff --git a/lib/circles/actionprocessor/factory.v b/lib/circles/actionprocessor/factory.v index de11fa38..75f8e748 100644 --- a/lib/circles/actionprocessor/factory.v +++ b/lib/circles/actionprocessor/factory.v @@ -1,8 +1,9 @@ module actionprocessor -import freeflowuniverse.herolib.circles.dbs.core -import freeflowuniverse.herolib.circles.dbs.mcc +import freeflowuniverse.herolib.circles.core.db +import freeflowuniverse.herolib.circles.mcc.db +import freeflowuniverse.herolib.circles.actions.db import freeflowuniverse.herolib.circles.models import freeflowuniverse.herolib.core.texttools @@ -17,11 +18,12 @@ __global ( pub struct CircleCoordinator { pub mut: name string //is a unique name on planetary scale is a dns name - agents &core.AgentDB - circles &core.CircleDB - names &core.NameDB - mails &mcc.MailDB - calendar &mcc.CalendarDB + agents &db.AgentDB + circles &db.CircleDB + names &db.NameDB + mails &db.MailDB + calendar &db.CalendarDB + jobs &db.JobDB session_state models.SessionState } @@ -61,6 +63,7 @@ pub fn new(args_ CircleCoordinatorArgs) !&CircleCoordinator { mut name_db := core.new_namedb(session_state)! mut mail_db := mcc.new_maildb(session_state)! mut calendar_db := mcc.new_calendardb(session_state)! + mut job_db := actions.new_jobdb(session_state)! mut cm := &CircleCoordinator{ agents: &agent_db @@ -68,6 +71,7 @@ pub fn new(args_ CircleCoordinatorArgs) !&CircleCoordinator { names: &name_db mails: &mail_db calendar: &calendar_db + jobs: &job_db session_state: session_state } diff --git a/lib/circles/actions/db/job_db.v b/lib/circles/actions/db/job_db.v new file mode 100644 index 00000000..b87c0767 --- /dev/null +++ b/lib/circles/actions/db/job_db.v @@ -0,0 +1,109 @@ +module actions + +import freeflowuniverse.herolib.circles.base { models } +import freeflowuniverse.herolib.circles.actions.models { Job, job_loads } + +@[heap] +pub struct JobDB { +pub mut: + db models.DBHandler[Job] +} + +pub fn new_jobdb(session_state models.SessionState) !JobDB { + return JobDB{ + db: models.new_dbhandler[Job]('job', session_state) + } +} + +pub fn (mut m JobDB) new() Job { + return Job{} +} + +// set adds or updates a job +pub fn (mut m JobDB) set(job Job) !Job { + return m.db.set(job)! +} + +// get retrieves a job by its ID +pub fn (mut m JobDB) get(id u32) !Job { + return m.db.get(id)! +} + +// list returns all job IDs +pub fn (mut m JobDB) list() ![]u32 { + return m.db.list()! +} + +pub fn (mut m JobDB) getall() ![]Job { + return m.db.getall()! +} + +// delete removes a job by its ID +pub fn (mut m JobDB) delete(id u32) ! { + m.db.delete(id)! +} + +//////////////////CUSTOM METHODS////////////////////////////////// + +// get_by_guid retrieves a job by its GUID +pub fn (mut m JobDB) get_by_guid(guid string) !Job { + return m.db.get_by_key('guid', guid)! +} + +// delete_by_guid removes a job by its GUID +pub fn (mut m JobDB) delete_by_guid(guid string) ! { + // Get the job by GUID + job := m.get_by_guid(guid) or { + // Job not found, nothing to delete + return + } + + // Delete the job by ID + m.delete(job.id)! +} + +// get_by_actor retrieves all jobs for a specific actor +pub fn (mut m JobDB) get_by_actor(actor string) ![]Job { + // Get all jobs with this actor + return m.db.getall_by_prefix('actor', actor)! +} + +// get_by_circle retrieves all jobs for a specific circle +pub fn (mut m JobDB) get_by_circle(circle string) ![]Job { + // Get all jobs with this circle + return m.db.getall_by_prefix('circle', circle)! +} + +// get_by_context retrieves all jobs for a specific context +pub fn (mut m JobDB) get_by_context(context string) ![]Job { + // Get all jobs with this context + return m.db.getall_by_prefix('context', context)! +} + +// get_by_circle_and_context retrieves all jobs for a specific circle and context +pub fn (mut m JobDB) get_by_circle_and_context(circle string, context string) ![]Job { + // Get all jobs for this circle + circle_jobs := m.get_by_circle(circle)! + + // Filter for the specific context + mut result := []Job{} + for job in circle_jobs { + if job.context == context { + result << job + } + } + + return result +} + +// update_job_status updates the status of a job +pub fn (mut m JobDB) update_job_status(guid string, new_status models.Status) !Job { + // Get the job by GUID + mut job := m.get_by_guid(guid)! + + // Update the job status + job.status.status = new_status + + // Save the updated job + return m.set(job)! +} diff --git a/lib/circles/actions/db/job_db_test.v b/lib/circles/actions/db/job_db_test.v new file mode 100644 index 00000000..e91e4b5e --- /dev/null +++ b/lib/circles/actions/db/job_db_test.v @@ -0,0 +1,211 @@ +module actions + +import os +import rand +import freeflowuniverse.herolib.circles.actionprocessor +import freeflowuniverse.herolib.circles.actions.models +import freeflowuniverse.herolib.data.ourtime + +fn test_job_db() { + // Create a temporary directory for testing + test_dir := os.join_path(os.temp_dir(), 'hero_job_test_${rand.intn(9000) or { 0 } + 1000}') + os.mkdir_all(test_dir) or { panic(err) } + defer { os.rmdir_all(test_dir) or {} } + + mut runner := actionprocessor.new(path: test_dir)! + + // Create multiple jobs for testing + mut job1 := runner.jobs.new() + job1.guid = 'job-1' + job1.actor = 'vm_manager' + job1.action = 'start' + job1.circle = 'circle1' + job1.context = 'context1' + job1.agents = ['agent1', 'agent2'] + job1.source = 'source1' + job1.params = { + 'id': '10' + 'name': 'test-vm' + } + job1.status.guid = job1.guid + job1.status.created = ourtime.now() + job1.status.status = .created + + mut job2 := runner.jobs.new() + job2.guid = 'job-2' + job2.actor = 'vm_manager' + job2.action = 'stop' + job2.circle = 'circle1' + job2.context = 'context2' + job2.agents = ['agent1'] + job2.source = 'source1' + job2.params = { + 'id': '11' + 'name': 'test-vm-2' + } + job2.status.guid = job2.guid + job2.status.created = ourtime.now() + job2.status.status = .created + + mut job3 := runner.jobs.new() + job3.guid = 'job-3' + job3.actor = 'network_manager' + job3.action = 'create' + job3.circle = 'circle2' + job3.context = 'context1' + job3.agents = ['agent3'] + job3.source = 'source2' + job3.params = { + 'name': 'test-network' + 'type': 'bridge' + } + job3.status.guid = job3.guid + job3.status.created = ourtime.now() + job3.status.status = .created + + // Add the jobs + println('Adding job 1') + job1 = runner.jobs.set(job1)! + + // Explicitly set different IDs for each job to avoid overwriting + job2.id = 1 // Set a different ID for job2 + println('Adding job 2') + job2 = runner.jobs.set(job2)! + + job3.id = 2 // Set a different ID for job3 + println('Adding job 3') + job3 = runner.jobs.set(job3)! + + // Test list functionality + println('Testing list functionality') + + // Get all jobs + all_jobs := runner.jobs.getall()! + println('Retrieved ${all_jobs.len} jobs') + for i, job in all_jobs { + println('Job ${i}: id=${job.id}, guid=${job.guid}, actor=${job.actor}') + } + + assert all_jobs.len == 3, 'Expected 3 jobs, got ${all_jobs.len}' + + // Verify all jobs are in the list + mut found1 := false + mut found2 := false + mut found3 := false + + for job in all_jobs { + if job.guid == 'job-1' { + found1 = true + } else if job.guid == 'job-2' { + found2 = true + } else if job.guid == 'job-3' { + found3 = true + } + } + + assert found1, 'Job 1 not found in list' + assert found2, 'Job 2 not found in list' + assert found3, 'Job 3 not found in list' + + // Get and verify individual jobs + println('Verifying individual jobs') + retrieved_job1 := runner.jobs.get_by_guid('job-1')! + assert retrieved_job1.guid == job1.guid + assert retrieved_job1.actor == job1.actor + assert retrieved_job1.action == job1.action + assert retrieved_job1.circle == job1.circle + assert retrieved_job1.context == job1.context + assert retrieved_job1.agents.len == 2 + assert retrieved_job1.agents[0] == 'agent1' + assert retrieved_job1.agents[1] == 'agent2' + assert retrieved_job1.params['id'] == '10' + assert retrieved_job1.params['name'] == 'test-vm' + assert retrieved_job1.status.status == .created + + // Test get_by_actor method + println('Testing get_by_actor method') + vm_manager_jobs := runner.jobs.get_by_actor('vm_manager')! + assert vm_manager_jobs.len == 2 + assert vm_manager_jobs[0].guid in ['job-1', 'job-2'] + assert vm_manager_jobs[1].guid in ['job-1', 'job-2'] + + // Test get_by_circle method + println('Testing get_by_circle method') + circle1_jobs := runner.jobs.get_by_circle('circle1')! + assert circle1_jobs.len == 2 + assert circle1_jobs[0].guid in ['job-1', 'job-2'] + assert circle1_jobs[1].guid in ['job-1', 'job-2'] + + // Test get_by_context method + println('Testing get_by_context method') + context1_jobs := runner.jobs.get_by_context('context1')! + assert context1_jobs.len == 2 + assert context1_jobs[0].guid in ['job-1', 'job-3'] + assert context1_jobs[1].guid in ['job-1', 'job-3'] + + // Test get_by_circle_and_context method + println('Testing get_by_circle_and_context method') + circle1_context1_jobs := runner.jobs.get_by_circle_and_context('circle1', 'context1')! + assert circle1_context1_jobs.len == 1 + assert circle1_context1_jobs[0].guid == 'job-1' + + // Test update_job_status method + println('Testing update_job_status method') + updated_job1 := runner.jobs.update_job_status('job-1', .running)! + assert updated_job1.status.status == .running + + // Verify the status was updated in the database + status_updated_job1 := runner.jobs.get_by_guid('job-1')! + assert status_updated_job1.status.status == .running + + // Test delete functionality + println('Testing delete functionality') + // Delete job 2 + runner.jobs.delete_by_guid('job-2')! + + // Verify deletion with list + jobs_after_delete := runner.jobs.getall()! + assert jobs_after_delete.len == 2, 'Expected 2 jobs after deletion, got ${jobs_after_delete.len}' + + // Verify the remaining jobs + mut found_after_delete1 := false + mut found_after_delete2 := false + mut found_after_delete3 := false + + for job in jobs_after_delete { + if job.guid == 'job-1' { + found_after_delete1 = true + } else if job.guid == 'job-2' { + found_after_delete2 = true + } else if job.guid == 'job-3' { + found_after_delete3 = true + } + } + + assert found_after_delete1, 'Job 1 not found after deletion' + assert !found_after_delete2, 'Job 2 found after deletion (should be deleted)' + assert found_after_delete3, 'Job 3 not found after deletion' + + // Delete another job + println('Deleting another job') + runner.jobs.delete_by_guid('job-3')! + + // Verify only one job remains + jobs_after_second_delete := runner.jobs.getall()! + assert jobs_after_second_delete.len == 1, 'Expected 1 job after second deletion, got ${jobs_after_second_delete.len}' + assert jobs_after_second_delete[0].guid == 'job-1', 'Remaining job should be job-1' + + // Delete the last job + println('Deleting last job') + runner.jobs.delete_by_guid('job-1')! + + // Verify no jobs remain + jobs_after_all_deleted := runner.jobs.getall() or { + // This is expected to fail with 'No jobs found' error + assert err.msg().contains('No index keys defined for this type') || err.msg().contains('No jobs found') + []models.Job{cap: 0} + } + assert jobs_after_all_deleted.len == 0, 'Expected 0 jobs after all deletions, got ${jobs_after_all_deleted.len}' + + println('All tests passed successfully') +} diff --git a/lib/circles/actions/models/job.v b/lib/circles/actions/models/job.v new file mode 100644 index 00000000..4db7dd6c --- /dev/null +++ b/lib/circles/actions/models/job.v @@ -0,0 +1,218 @@ +module models + +import freeflowuniverse.herolib.data.ourtime +import freeflowuniverse.herolib.data.encoder + +// Job represents a task to be executed by an agent +pub struct Job { +pub mut: + id u32 // unique numeric id for the job + guid string // unique id for the job + agents []string // the pub key of the agent(s) which will execute the command, only 1 will execute + source string // pubkey from the agent who asked for the job + circle string = 'default' // our digital life is organized in circles + context string = 'default' // is the high level context in which actors will execute the work inside a circle + actor string // e.g. vm_manager + action string // e.g. start + params map[string]string // e.g. id:10 + timeout_schedule u16 = 60 // timeout before its picked up + timeout u16 = 3600 // timeout in sec + log bool = true + ignore_error bool // means if error will just exit and not raise, there will be no error reporting + ignore_error_codes []int // of we want to ignore certain error codes + debug bool // if debug will get more context + retry int // default there is no debug + status JobStatus + dependencies []JobDependency // will not execute until other jobs are done +} + +// JobStatus represents the current state of a job +pub struct JobStatus { +pub mut: + guid string // unique id for the job + created ourtime.OurTime // when we created the job + start ourtime.OurTime // when the job needs to start + end ourtime.OurTime // when the job ended, can be in error + status Status // current status of the job +} + +// JobDependency represents a dependency on another job +pub struct JobDependency { +pub mut: + guid string // unique id for the job + agents []string // the pub key of the agent(s) which can execute the command +} + +// Status represents the possible states of a job +pub enum Status { + created // initial state + scheduled // job has been scheduled + planned // arrived where actor will execute the job + running // job is currently running + error // job encountered an error + ok // job completed successfully +} + +pub fn (j Job) index_keys() map[string]string { + return { + 'guid': j.guid, + 'actor': j.actor, + 'circle': j.circle, + 'context': j.context + } +} + +// dumps serializes the Job struct to binary format using the encoder +// This implements the Serializer interface +pub fn (j Job) dumps() ![]u8 { + mut e := encoder.new() + + // Add unique encoding ID to identify this type of data + e.add_u16(300) + + // Encode Job fields + e.add_u32(j.id) + e.add_string(j.guid) + + // Encode agents array + e.add_u16(u16(j.agents.len)) + for agent in j.agents { + e.add_string(agent) + } + + e.add_string(j.source) + e.add_string(j.circle) + e.add_string(j.context) + e.add_string(j.actor) + e.add_string(j.action) + + // Encode params map + e.add_u16(u16(j.params.len)) + for key, value in j.params { + e.add_string(key) + e.add_string(value) + } + + e.add_u16(j.timeout_schedule) + e.add_u16(j.timeout) + e.add_bool(j.log) + e.add_bool(j.ignore_error) + + // Encode ignore_error_codes array + e.add_u16(u16(j.ignore_error_codes.len)) + for code in j.ignore_error_codes { + e.add_i32(code) + } + + e.add_bool(j.debug) + e.add_i32(j.retry) + + // Encode JobStatus + e.add_string(j.status.guid) + e.add_i64(j.status.created.unix) + e.add_i64(j.status.start.unix) + e.add_i64(j.status.end.unix) + e.add_u8(u8(j.status.status)) + + // Encode dependencies array + e.add_u16(u16(j.dependencies.len)) + for dependency in j.dependencies { + e.add_string(dependency.guid) + + // Encode dependency agents array + e.add_u16(u16(dependency.agents.len)) + for agent in dependency.agents { + e.add_string(agent) + } + } + + return e.data +} + +// loads deserializes binary data into a Job struct +pub fn job_loads(data []u8) !Job { + mut d := encoder.decoder_new(data) + mut job := Job{} + + // Check encoding ID to verify this is the correct type of data + encoding_id := d.get_u16()! + if encoding_id != 300 { + return error('Wrong file type: expected encoding ID 300, got ${encoding_id}, for job') + } + + // Decode Job fields + job.id = d.get_u32()! + job.guid = d.get_string()! + + // Decode agents array + agents_len := d.get_u16()! + job.agents = []string{len: int(agents_len)} + for i in 0 .. agents_len { + job.agents[i] = d.get_string()! + } + + job.source = d.get_string()! + job.circle = d.get_string()! + job.context = d.get_string()! + job.actor = d.get_string()! + job.action = d.get_string()! + + // Decode params map + params_len := d.get_u16()! + job.params = map[string]string{} + for _ in 0 .. params_len { + key := d.get_string()! + value := d.get_string()! + job.params[key] = value + } + + job.timeout_schedule = d.get_u16()! + job.timeout = d.get_u16()! + job.log = d.get_bool()! + job.ignore_error = d.get_bool()! + + // Decode ignore_error_codes array + error_codes_len := d.get_u16()! + job.ignore_error_codes = []int{len: int(error_codes_len)} + for i in 0 .. error_codes_len { + job.ignore_error_codes[i] = d.get_i32()! + } + + job.debug = d.get_bool()! + job.retry = d.get_i32()! + + // Decode JobStatus + job.status.guid = d.get_string()! + job.status.created.unix = d.get_i64()! + job.status.start.unix = d.get_i64()! + job.status.end.unix = d.get_i64()! + status_val := d.get_u8()! + job.status.status = match status_val { + 0 { Status.created } + 1 { Status.scheduled } + 2 { Status.planned } + 3 { Status.running } + 4 { Status.error } + 5 { Status.ok } + else { return error('Invalid Status value: ${status_val}') } + } + + // Decode dependencies array + dependencies_len := d.get_u16()! + job.dependencies = []JobDependency{len: int(dependencies_len)} + for i in 0 .. dependencies_len { + mut dependency := JobDependency{} + dependency.guid = d.get_string()! + + // Decode dependency agents array + dep_agents_len := d.get_u16()! + dependency.agents = []string{len: int(dep_agents_len)} + for j in 0 .. dep_agents_len { + dependency.agents[j] = d.get_string()! + } + + job.dependencies[i] = dependency + } + + return job +} diff --git a/lib/circles/actions/models/job_test.v b/lib/circles/actions/models/job_test.v new file mode 100644 index 00000000..d3a20efe --- /dev/null +++ b/lib/circles/actions/models/job_test.v @@ -0,0 +1,206 @@ +module model + +import freeflowuniverse.herolib.data.ourtime + +fn test_job_serialization() { + // Create a test job + mut job := Job{ + id: 1 + guid: 'test-job-1' + agents: ['agent1', 'agent2'] + source: 'source1' + circle: 'test-circle' + context: 'test-context' + actor: 'vm_manager' + action: 'start' + params: { + 'id': '10' + 'name': 'test-vm' + } + timeout_schedule: 120 + timeout: 7200 + log: true + ignore_error: false + ignore_error_codes: [404, 500] + debug: true + retry: 3 + } + + // Set up job status + job.status = JobStatus{ + guid: job.guid + created: ourtime.now() + start: ourtime.now() + end: ourtime.OurTime{} + status: .created + } + + // Add a dependency + job.dependencies << JobDependency{ + guid: 'dependency-job-1' + agents: ['agent1'] + } + + // Test index_keys method + keys := job.index_keys() + assert keys['guid'] == 'test-job-1' + assert keys['actor'] == 'vm_manager' + assert keys['circle'] == 'test-circle' + assert keys['context'] == 'test-context' + + // Serialize the job + println('Serializing job...') + serialized := job.dumps() or { + assert false, 'Failed to serialize job: ${err}' + return + } + assert serialized.len > 0, 'Serialized data should not be empty' + + // Deserialize the job + println('Deserializing job...') + deserialized := job_loads(serialized) or { + assert false, 'Failed to deserialize job: ${err}' + return + } + + // Verify the deserialized job + assert deserialized.id == job.id + assert deserialized.guid == job.guid + assert deserialized.agents.len == job.agents.len + assert deserialized.agents[0] == job.agents[0] + assert deserialized.agents[1] == job.agents[1] + assert deserialized.source == job.source + assert deserialized.circle == job.circle + assert deserialized.context == job.context + assert deserialized.actor == job.actor + assert deserialized.action == job.action + assert deserialized.params.len == job.params.len + assert deserialized.params['id'] == job.params['id'] + assert deserialized.params['name'] == job.params['name'] + assert deserialized.timeout_schedule == job.timeout_schedule + assert deserialized.timeout == job.timeout + assert deserialized.log == job.log + assert deserialized.ignore_error == job.ignore_error + assert deserialized.ignore_error_codes.len == job.ignore_error_codes.len + assert deserialized.ignore_error_codes[0] == job.ignore_error_codes[0] + assert deserialized.ignore_error_codes[1] == job.ignore_error_codes[1] + assert deserialized.debug == job.debug + assert deserialized.retry == job.retry + assert deserialized.status.guid == job.status.guid + assert deserialized.status.status == job.status.status + assert deserialized.dependencies.len == job.dependencies.len + assert deserialized.dependencies[0].guid == job.dependencies[0].guid + assert deserialized.dependencies[0].agents.len == job.dependencies[0].agents.len + assert deserialized.dependencies[0].agents[0] == job.dependencies[0].agents[0] + + println('All job serialization tests passed!') +} + +fn test_job_status_enum() { + // Test all status enum values + assert u8(Status.created) == 0 + assert u8(Status.scheduled) == 1 + assert u8(Status.planned) == 2 + assert u8(Status.running) == 3 + assert u8(Status.error) == 4 + assert u8(Status.ok) == 5 + + // Test status progression + mut status := Status.created + assert status == .created + + status = .scheduled + assert status == .scheduled + + status = .planned + assert status == .planned + + status = .running + assert status == .running + + status = .error + assert status == .error + + status = .ok + assert status == .ok + + println('All job status enum tests passed!') +} + +fn test_job_dependency() { + // Create a test dependency + mut dependency := JobDependency{ + guid: 'dependency-job-1' + agents: ['agent1', 'agent2', 'agent3'] + } + + // Create a job with this dependency + mut job := Job{ + id: 2 + guid: 'test-job-2' + actor: 'network_manager' + action: 'create' + dependencies: [dependency] + } + + // Test dependency properties + assert job.dependencies.len == 1 + assert job.dependencies[0].guid == 'dependency-job-1' + assert job.dependencies[0].agents.len == 3 + assert job.dependencies[0].agents[0] == 'agent1' + assert job.dependencies[0].agents[1] == 'agent2' + assert job.dependencies[0].agents[2] == 'agent3' + + // Add another dependency + job.dependencies << JobDependency{ + guid: 'dependency-job-2' + agents: ['agent4'] + } + + // Test multiple dependencies + assert job.dependencies.len == 2 + assert job.dependencies[1].guid == 'dependency-job-2' + assert job.dependencies[1].agents.len == 1 + assert job.dependencies[1].agents[0] == 'agent4' + + println('All job dependency tests passed!') +} + +fn test_job_with_empty_values() { + // Create a job with minimal values + mut job := Job{ + id: 3 + guid: 'minimal-job' + actor: 'minimal_actor' + action: 'test' + } + + // Serialize and deserialize + serialized := job.dumps() or { + assert false, 'Failed to serialize minimal job: ${err}' + return + } + + deserialized := job_loads(serialized) or { + assert false, 'Failed to deserialize minimal job: ${err}' + return + } + + // Verify defaults are preserved + assert deserialized.id == job.id + assert deserialized.guid == job.guid + assert deserialized.circle == 'default' // Default value + assert deserialized.context == 'default' // Default value + assert deserialized.actor == 'minimal_actor' + assert deserialized.action == 'test' + assert deserialized.agents.len == 0 + assert deserialized.params.len == 0 + assert deserialized.timeout_schedule == 60 // Default value + assert deserialized.timeout == 3600 // Default value + assert deserialized.log == true // Default value + assert deserialized.ignore_error == false // Default value + assert deserialized.ignore_error_codes.len == 0 + assert deserialized.dependencies.len == 0 + + println('All minimal job tests passed!') +} diff --git a/lib/circles/models/dbhandler.v b/lib/circles/base/dbhandler.v similarity index 94% rename from lib/circles/models/dbhandler.v rename to lib/circles/base/dbhandler.v index 1ff97d37..069f9fb8 100644 --- a/lib/circles/models/dbhandler.v +++ b/lib/circles/base/dbhandler.v @@ -1,7 +1,8 @@ module models -import freeflowuniverse.herolib.circles.models.core { agent_loads, Agent, circle_loads, Circle, name_loads, Name } -import freeflowuniverse.herolib.circles.models.mcc { Email, email_loads, CalendarEvent, calendar_event_loads } +import freeflowuniverse.herolib.circles.core.models { agent_loads, Agent, circle_loads, Circle, name_loads, Name } +import freeflowuniverse.herolib.circles.mcc.models { Email, email_loads, CalendarEvent, calendar_event_loads } +import freeflowuniverse.herolib.circles.actions.models { Job, job_loads } pub struct DBHandler[T] { pub mut: @@ -62,6 +63,10 @@ pub fn (mut m DBHandler[T]) get(id u32) !T { mut o:= calendar_event_loads(item_data)! o.id = id return o + } $else $if T is Job { + mut o:= job_loads(item_data)! + o.id = id + return o } $else { return error('Unsupported type for deserialization') } diff --git a/lib/circles/models/sessionstate.v b/lib/circles/base/sessionstate.v similarity index 100% rename from lib/circles/models/sessionstate.v rename to lib/circles/base/sessionstate.v diff --git a/lib/circles/dbs/core/agent_db.v b/lib/circles/core/db/agent_db.v similarity index 97% rename from lib/circles/dbs/core/agent_db.v rename to lib/circles/core/db/agent_db.v index 5d53d038..47c9d60a 100644 --- a/lib/circles/dbs/core/agent_db.v +++ b/lib/circles/core/db/agent_db.v @@ -2,7 +2,7 @@ module core import freeflowuniverse.herolib.data.ourtime import freeflowuniverse.herolib.circles.models { DBHandler, SessionState } -import freeflowuniverse.herolib.circles.models.core { Agent, AgentService, AgentServiceAction, AgentState } +import freeflowuniverse.herolib.circles.core.models { Agent, AgentService, AgentServiceAction, AgentState } @[heap] diff --git a/lib/circles/dbs/core/agent_db_test.v b/lib/circles/core/db/agent_db_test.v similarity index 99% rename from lib/circles/dbs/core/agent_db_test.v rename to lib/circles/core/db/agent_db_test.v index 7ba2bbaf..4af0c72a 100755 --- a/lib/circles/dbs/core/agent_db_test.v +++ b/lib/circles/core/db/agent_db_test.v @@ -3,7 +3,8 @@ module core import os import rand import freeflowuniverse.herolib.circles.actionprocessor -import freeflowuniverse.herolib.circles.models.core +import freeflowuniverse.herolib.circles.core.model + fn test_agent_db() { // Create a temporary directory for testing test_dir := os.join_path(os.temp_dir(), 'hero_agent_test_${rand.intn(9000) or { 0 } + 1000}') diff --git a/lib/circles/dbs/core/circle_db.v b/lib/circles/core/db/circle_db.v similarity index 98% rename from lib/circles/dbs/core/circle_db.v rename to lib/circles/core/db/circle_db.v index 0b3b1d68..402d0893 100644 --- a/lib/circles/dbs/core/circle_db.v +++ b/lib/circles/core/db/circle_db.v @@ -1,7 +1,7 @@ module core import freeflowuniverse.herolib.circles.models { DBHandler, SessionState } -import freeflowuniverse.herolib.circles.models.core { Circle } +import freeflowuniverse.herolib.circles.core.models { Circle } @[heap] pub struct CircleDB { diff --git a/lib/circles/dbs/core/circle_db_test.v b/lib/circles/core/db/circle_db_test.v similarity index 99% rename from lib/circles/dbs/core/circle_db_test.v rename to lib/circles/core/db/circle_db_test.v index b9582c57..11536440 100644 --- a/lib/circles/dbs/core/circle_db_test.v +++ b/lib/circles/core/db/circle_db_test.v @@ -3,7 +3,7 @@ module core import os import rand import freeflowuniverse.herolib.circles.actionprocessor -import freeflowuniverse.herolib.circles.models.core +import freeflowuniverse.herolib.circles.core.models fn test_circle_db() { // Create a temporary directory for testing diff --git a/lib/circles/dbs/core/name_db.v b/lib/circles/core/db/name_db.v similarity index 98% rename from lib/circles/dbs/core/name_db.v rename to lib/circles/core/db/name_db.v index bb9066a7..374dc634 100644 --- a/lib/circles/dbs/core/name_db.v +++ b/lib/circles/core/db/name_db.v @@ -1,7 +1,7 @@ module core import freeflowuniverse.herolib.circles.models { DBHandler, SessionState } -import freeflowuniverse.herolib.circles.models.core { Name, Record, RecordType } +import freeflowuniverse.herolib.circles.core.models { Name, Record, RecordType } @[heap] pub struct NameDB { diff --git a/lib/circles/dbs/core/name_db_test.v b/lib/circles/core/db/name_db_test.v similarity index 99% rename from lib/circles/dbs/core/name_db_test.v rename to lib/circles/core/db/name_db_test.v index 94be4d0d..72e7e577 100644 --- a/lib/circles/dbs/core/name_db_test.v +++ b/lib/circles/core/db/name_db_test.v @@ -3,7 +3,7 @@ module core import os import rand import freeflowuniverse.herolib.circles.actionprocessor -import freeflowuniverse.herolib.circles.models.core +import freeflowuniverse.herolib.circles.core.models fn test_name_db() { // Create a temporary directory for testing diff --git a/lib/circles/models/core/README.md b/lib/circles/core/models/README.md similarity index 100% rename from lib/circles/models/core/README.md rename to lib/circles/core/models/README.md diff --git a/lib/circles/models/core/agent.v b/lib/circles/core/models/agent.v similarity index 100% rename from lib/circles/models/core/agent.v rename to lib/circles/core/models/agent.v diff --git a/lib/circles/models/core/agent_test.v b/lib/circles/core/models/agent_test.v similarity index 100% rename from lib/circles/models/core/agent_test.v rename to lib/circles/core/models/agent_test.v diff --git a/lib/circles/models/core/circle.v b/lib/circles/core/models/circle.v similarity index 100% rename from lib/circles/models/core/circle.v rename to lib/circles/core/models/circle.v diff --git a/lib/circles/models/core/circle_test.v b/lib/circles/core/models/circle_test.v similarity index 100% rename from lib/circles/models/core/circle_test.v rename to lib/circles/core/models/circle_test.v diff --git a/lib/circles/models/core/name.v b/lib/circles/core/models/name.v similarity index 100% rename from lib/circles/models/core/name.v rename to lib/circles/core/models/name.v diff --git a/lib/circles/models/core/name_test.v b/lib/circles/core/models/name_test.v similarity index 100% rename from lib/circles/models/core/name_test.v rename to lib/circles/core/models/name_test.v diff --git a/lib/circles/dbs/mcc/calendar_db.v b/lib/circles/mcc/db/calendar_db.v similarity index 98% rename from lib/circles/dbs/mcc/calendar_db.v rename to lib/circles/mcc/db/calendar_db.v index a24b88ec..bb2ecf69 100644 --- a/lib/circles/dbs/mcc/calendar_db.v +++ b/lib/circles/mcc/db/calendar_db.v @@ -1,7 +1,7 @@ module mcc import freeflowuniverse.herolib.circles.models { DBHandler, SessionState } -import freeflowuniverse.herolib.circles.models.mcc { CalendarEvent, calendar_event_loads } +import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent, calendar_event_loads } @[heap] pub struct CalendarDB { diff --git a/lib/circles/dbs/mcc/calendar_db_test.v b/lib/circles/mcc/db/calendar_db_test.v similarity index 98% rename from lib/circles/dbs/mcc/calendar_db_test.v rename to lib/circles/mcc/db/calendar_db_test.v index 859c4fdc..147c3206 100644 --- a/lib/circles/dbs/mcc/calendar_db_test.v +++ b/lib/circles/mcc/db/calendar_db_test.v @@ -1,7 +1,7 @@ module mcc import freeflowuniverse.herolib.circles.models { SessionState, new_session } -import freeflowuniverse.herolib.circles.models.mcc { CalendarEvent } +import freeflowuniverse.herolib.circles.mcc.models { CalendarEvent } import freeflowuniverse.herolib.data.ourtime import os import rand diff --git a/lib/circles/dbs/mcc/mail_db.v b/lib/circles/mcc/db/mail_db.v similarity index 98% rename from lib/circles/dbs/mcc/mail_db.v rename to lib/circles/mcc/db/mail_db.v index d0fd123a..f65cf32f 100644 --- a/lib/circles/dbs/mcc/mail_db.v +++ b/lib/circles/mcc/db/mail_db.v @@ -1,7 +1,7 @@ module mcc import freeflowuniverse.herolib.circles.models { DBHandler, SessionState } -import freeflowuniverse.herolib.circles.models.mcc { Email, email_loads } +import freeflowuniverse.herolib.circles.mcc.models { Email, email_loads } @[heap] pub struct MailDB { diff --git a/lib/circles/dbs/mcc/mail_db_test.v b/lib/circles/mcc/db/mail_db_test.v similarity index 99% rename from lib/circles/dbs/mcc/mail_db_test.v rename to lib/circles/mcc/db/mail_db_test.v index 45e90c1c..0b441cfa 100644 --- a/lib/circles/dbs/mcc/mail_db_test.v +++ b/lib/circles/mcc/db/mail_db_test.v @@ -3,7 +3,7 @@ module mcc import os import rand import freeflowuniverse.herolib.circles.actionprocessor -import freeflowuniverse.herolib.circles.models.mcc +import freeflowuniverse.herolib.circles.mcc.models fn test_mail_db() { // Create a temporary directory for testing diff --git a/lib/circles/models/mcc/caledar.v b/lib/circles/mcc/models/caledar.v similarity index 100% rename from lib/circles/models/mcc/caledar.v rename to lib/circles/mcc/models/caledar.v diff --git a/lib/circles/models/mcc/calendar_test.v b/lib/circles/mcc/models/calendar_test.v similarity index 100% rename from lib/circles/models/mcc/calendar_test.v rename to lib/circles/mcc/models/calendar_test.v diff --git a/lib/circles/models/mcc/mail.v b/lib/circles/mcc/models/mail.v similarity index 100% rename from lib/circles/models/mcc/mail.v rename to lib/circles/mcc/models/mail.v diff --git a/lib/circles/models/mcc/mail_simple_test.v b/lib/circles/mcc/models/mail_simple_test.v similarity index 100% rename from lib/circles/models/mcc/mail_simple_test.v rename to lib/circles/mcc/models/mail_simple_test.v diff --git a/lib/circles/models/mcc/mail_test.v b/lib/circles/mcc/models/mail_test.v similarity index 100% rename from lib/circles/models/mcc/mail_test.v rename to lib/circles/mcc/models/mail_test.v diff --git a/lib/circles/models/jobs/job.v b/lib/circles/models/jobs/job.v deleted file mode 100644 index 87bf99ad..00000000 --- a/lib/circles/models/jobs/job.v +++ /dev/null @@ -1,52 +0,0 @@ -module model - -import freeflowuniverse.herolib.data.ourtime - -// Job represents a task to be executed by an agent -pub struct Job { -pub mut: - guid string // unique id for the job - agents []string // the pub key of the agent(s) which will execute the command, only 1 will execute - source string // pubkey from the agent who asked for the job - circle string = 'default' // our digital life is organized in circles - context string = 'default' // is the high level context in which actors will execute the work inside a circle - actor string // e.g. vm_manager - action string // e.g. start - params map[string]string // e.g. id:10 - timeout_schedule u16 = 60 // timeout before its picked up - timeout u16 = 3600 // timeout in sec - log bool = true - ignore_error bool // means if error will just exit and not raise, there will be no error reporting - ignore_error_codes []int // of we want to ignore certain error codes - debug bool // if debug will get more context - retry int // default there is no debug - status JobStatus - dependencies []JobDependency // will not execute until other jobs are done -} - -// JobStatus represents the current state of a job -pub struct JobStatus { -pub mut: - guid string // unique id for the job - created ourtime.OurTime // when we created the job - start ourtime.OurTime // when the job needs to start - end ourtime.OurTime // when the job ended, can be in error - status Status // current status of the job -} - -// JobDependency represents a dependency on another job -pub struct JobDependency { -pub mut: - guid string // unique id for the job - agents []string // the pub key of the agent(s) which can execute the command -} - -// Status represents the possible states of a job -pub enum Status { - created // initial state - scheduled // job has been scheduled - planned // arrived where actor will execute the job - running // job is currently running - error // job encountered an error - ok // job completed successfully -} diff --git a/lib/data/encoder/auto.v b/lib/data/encoder/auto.v index 102399be..75b2ec8d 100644 --- a/lib/data/encoder/auto.v +++ b/lib/data/encoder/auto.v @@ -13,6 +13,8 @@ pub fn encode[T](obj T) ![]u8 { $if field.typ is string { // $(string_expr) produces an identifier d.add_string(obj.$(field.name).str()) + } $else $if field.typ is bool { + d.add_bool(bool(obj.$(field.name))) } $else $if field.typ is int { d.add_int(int(obj.$(field.name))) } $else $if field.typ is u8 { @@ -70,6 +72,8 @@ pub fn decode[T](data []u8) !T { $if field.typ is string { // $(string_expr) produces an identifier result.$(field.name) = d.get_string()! + } $else $if field.typ is bool { + result.$(field.name) = d.get_bool()! } $else $if field.typ is int { result.$(field.name) = d.get_int()! } $else $if field.typ is u8 { diff --git a/lib/data/encoder/encoder_decode.v b/lib/data/encoder/encoder_decode.v index ac07ce2b..6b7653ce 100644 --- a/lib/data/encoder/encoder_decode.v +++ b/lib/data/encoder/encoder_decode.v @@ -54,6 +54,11 @@ pub fn (mut d Decoder) get_bytes() ![]u8 { return bytes } +pub fn (mut d Decoder) get_bool() !bool { + val := d.get_u8()! + return val == 1 +} + // adds u16 length of string in bytes + the bytes pub fn (mut d Decoder) get_u8() !u8 { if d.data.len < 1 { diff --git a/lib/data/encoder/encoder_encode.v b/lib/data/encoder/encoder_encode.v index 439c9f2d..94383e30 100644 --- a/lib/data/encoder/encoder_encode.v +++ b/lib/data/encoder/encoder_encode.v @@ -57,6 +57,14 @@ pub fn (mut b Encoder) add_bytes(data []u8) { b.data << data } +pub fn (mut b Encoder) add_bool(data bool) { + if data { + b.add_u8(1) + } else { + b.add_u8(0) + } +} + pub fn (mut b Encoder) add_u8(data u8) { b.data << data } diff --git a/lib/data/encoder/encoder_test.v b/lib/data/encoder/encoder_test.v index c1b63afc..debf7487 100644 --- a/lib/data/encoder/encoder_test.v +++ b/lib/data/encoder/encoder_test.v @@ -37,6 +37,17 @@ fn test_bytes() { assert d.get_list_u8()! == sb } +fn test_bool() { + mut e := new() + e.add_bool(true) + e.add_bool(false) + assert e.data == [u8(1), 0] + + mut d := decoder_new(e.data) + assert d.get_bool()! == true + assert d.get_bool()! == false +} + fn test_u8() { mut e := new() e.add_u8(min_u8) @@ -88,7 +99,8 @@ fn test_time() { e.add_time(t) mut d := decoder_new(e.data) - assert d.get_time()! == t + // Compare unix timestamps instead of full time objects + assert d.get_time()!.unix() == t.unix() } fn test_list_string() { @@ -198,7 +210,13 @@ fn encode_decode_struct[T](input StructType[T]) bool { console.print_debug('Failed to decode, error: ${err}') return false } - return input == output + + $if T is time.Time { + // Special handling for time.Time comparison + return input.val.unix() == output.val.unix() + } $else { + return input == output + } } fn test_struct() { @@ -230,6 +248,11 @@ fn test_struct() { // time.Time // assert encode_decode_struct[time.Time](get_empty_struct_input[time.Time]()) // get error here assert encode_decode_struct[time.Time](get_struct_input[time.Time](time.now())) + + // bool + assert encode_decode_struct(get_empty_struct_input[bool]()) + assert encode_decode_struct(get_struct_input(true)) + assert encode_decode_struct(get_struct_input(false)) // string array assert encode_decode_struct(get_empty_struct_input[[]string]()) diff --git a/lib/data/encoder/readme.md b/lib/data/encoder/readme.md index 326adfb4..a4d2bdc2 100644 --- a/lib/data/encoder/readme.md +++ b/lib/data/encoder/readme.md @@ -27,6 +27,7 @@ The binary format starts with a version byte (currently v1), followed by the enc ### Primitive Types - `string` - `int` (32-bit) +- `bool` - `u8` - `u16` - `u32` @@ -61,6 +62,7 @@ mut e := encoder.new() // Add primitive values e.add_string('hello') e.add_int(42) +e.add_bool(true) e.add_u8(255) e.add_u16(65535) e.add_u32(4294967295) @@ -89,6 +91,7 @@ mut d := encoder.decoder_new(encoded) // Read values in same order as encoded str := d.get_string() num := d.get_int() +bool_val := d.get_bool() byte := d.get_u8() u16_val := d.get_u16() u32_val := d.get_u32()