Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b14952e2 |
No files matched your search
@@ -5429,3 +5429,7 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23 | Boyd | Row 298 (#1804, Q228), targeted verification | On sage-3642 T1 lane B, five whole files serial: business scope 7/0, bus scope 14/0, scope runtime 1/0, CLI scope 4/0, project runtime 3/0 (29/0, no skips). Initial bus fixture typo recorded and corrected. M1-M10 killed by named assertions, source restored and final files green. Dewey dewey-0933 positive control and notes addressed; manifest and evidence in agents/boyd/work/queue-298/BUILD.md. Full package/script gate remains Sage's. No live scope set.
|
||||
|
||||
2026-10-23T22:41:07Z | Boyd | Row 298 (#1804), rebase after row 239 | Rebased onto 833ec1cc8d7c11842efb82e57c1a0b256c8d22d7 on boyd/row298-r2 without altering published boyd/row298. Only append-log conflicts; upstream bytes preserved and Boyd suffixes appended. Product merges automatic; row 239 writer-lock revoke and post-revoke error handling preserved, organization.mjs unchanged from new base. Static checks only; UNTESTED on new base pending Sage CLEAR. Details in Boyd BUILD.md.
|
||||
|
||||
2026-10-23 | Goldie | Row 324 / #1820 before (Q247, mos-1555) | New branch goldie/row324 at row298 approved860afc2b6. Design/readers audit in agents/goldie/work/queue-324/BUILD.md: normalize omitted tracker to null, preserve present validation, guard sync readers/adapter config; runtime/instance-settings/broker checks already optional. Existing business.test.mjs98 explicitly requires tracker, so implementation held for Mos approval of removing that one list entry; no old test or product edit, no tests run.
|
||||
|
||||
2026-10-23 | Goldie | Row 324 / #1820 draft built (sage-3836, mos-1556) | Claim rev3759. Omitted tracker normalizes null; present including null/malformed stays validated. Loader, business validate, bootConfig trackerFor and Console sync reader handle absence. Row298's guarded runtime/instance-settings/broker paths preserved. Two new test files (9 planned tests); sole old-test edit drops tracker from required-key list under mos-1556. node --check6/6 and diff check pass. No tests run: untested branch push expressly ordered before CLEAR; Duckworth review and Sage gate pending.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Optional business tracker (Q247) implementation plan — row 324 / #1820
|
||||
|
||||
Goldie, 2026-10-23. Base 860afc2b66e7bbd045e9d5e25d227ef12f62c55b, approved row298 candidate. New worktree /home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row324, branch goldie/row324. Spec: git object f375f4e92:docs/plans/2026-10-23_optional-tracker-brief.md, section Brief: tracker becomes optional in the business file. Sage-3836 and mos-1555/Q247 authorize optional tracker; Duckworth reviews credential registration.
|
||||
|
||||
## Goal and design
|
||||
|
||||
A raw business document may omit tracker. Normalize omission to tracker:null, consistent with existing optional launch/role-tracker representations. A present tracker (including null, false, malformed objects) must continue through checkTracker and its existing refusals; distinguish absence with Object.hasOwn rather than a truthiness check. Keep checkTracker unchanged. Required top-level keys no longer include tracker. Only compare role bots with the sync bot when a checked business tracker exists. Role definitions needing vikunja still require their own tracker and credentials unchanged.
|
||||
|
||||
CLI validate collects sync credential refs only when tracker is present. bootConfig's trackerFor returns null before interpreting tracker variables when business tracker is absent, so an old tracker.baseUrl variable cannot accidentally create sync/task adapter configuration. Console businessView emits no sync credential rows for absent tracker. Runtime registration, fixed-configuration reload comparison, instance-settings bot collision checks and broker credential display already use optional chaining on the row298 base; preserve those checks. The tasks adapter remains fail-closed for an explicitly inconsistent raw adapter config, and no-tracker bootConfig never configures it for this business.
|
||||
|
||||
## Reader audit (before implementation)
|
||||
|
||||
Command: git grep -n -E 'tracker\.sync|\.tracker\b' -- 'packages/*/src/*'. Every result is grouped here, with base line numbers:
|
||||
|
||||
| Reader | Absent business tracker behavior / action |
|
||||
| --- | --- |
|
||||
| business/src/business.mjs:85-96,296-300 | checkTracker remains exact for present input; add omission normalization and guard own-sync-bot comparison. |
|
||||
| business/src/business.mjs:143-150 | Per-role tracker, independent of business tracker. Keep requirement/collision checks unchanged. |
|
||||
| business/src/cli.mjs:85 | Guard Object.values sync credential refs; absent contributes zero. |
|
||||
| business/src/instance-settings.mjs:108,120,126-127,133,223 | Role add/change and bot collision/retirement: old.tracker?.sync already skips absent sync; role trackers unchanged. No edit needed. |
|
||||
| business/src/launch-settings.mjs:161,235-236 | Per-role tracker assistant view/update, independent of business tracker; no edit. |
|
||||
| business/src/resolve.mjs:93 | Per-role tracker in resolved output; no edit. |
|
||||
| bus/src/runtime.mjs:131 | Existing b.tracker?.sync?.credentials check registers no @sync when absent; preserve and exercise with new boot test. |
|
||||
| bus/src/runtime.mjs:243,247 | Reload compares per-role tracker/null and canonical business sync/null; safe already, no edit. |
|
||||
| bus/src/broker.mjs:3969-3970 | Optional per-role tracker bot map; independent, no edit. |
|
||||
| bus/src/broker.mjs:4497 | Optional per-role tracker returned from instance write; independent, no edit. |
|
||||
| bus/src/broker.mjs:6242 | Credential display resolves optional sync bot; no @sync row exists on trackerless boot. Safe already. |
|
||||
| cli/src/config.mjs:57-60,137-138 (trackerFor; not caught by literal grep) | Add business-tracker absence guard; do not configure tracker adapter even if tracker.baseUrl remains set. |
|
||||
| cli/src/instances.mjs:118 | Builds per-role tracker update request only when user supplies bot; unrelated to business tracker. |
|
||||
| tasks/src/adapter.mjs:25,28-30,35,45,81 | settings rejects missing sync with controlled tracker-config before dereferencing labels/botId; files uses optional chaining. Factory iterates only configured trackers; absent business never configured by bootConfig. Preserve explicit-inconsistent-config refusal. |
|
||||
| webui/src/reads.mjs:164,166 | Role credentials independent; guard sync credential display at166 so trackerless Console does not throw. |
|
||||
| webui/src/public/bus.js:785 | Per-role editor tracker patch; independent of business tracker. |
|
||||
| conversation/src/controller.mjs:443,502,748,807,820,831,888,935,961,997,1008,1021,1048,1056,1268,1308,1330,1334,1413,1447 | Different execution-tracking object/class, unrelated to business/Vikunja config. No change. |
|
||||
|
||||
Positive control: grep finds business.mjs:296 and webui reads:166, the unsafe business-tracker readers. Review focus: present null/malformed tracker still refuses; per-role Vikunja trackers still required; stale tracker variables do not enable sync; Console credential view remains usable; no @sync registration during runtime boot/reload.
|
||||
|
||||
## Steps and evidence plan
|
||||
|
||||
1. Document design and full reader list (this file); claim row324 in-progress as Goldie.
|
||||
2. Add new tests for absent tracker loader+CLI validate, malformed/present preservation, per-role tracker requirement, stale-variable bootConfig behavior, actual bus boot with empty credential list/no @sync, and Console business view. New test files only unless Mos expressly approves old-test change below.
|
||||
3. Make the minimal four-source-file change (business loader, business CLI, cli config, webui reads). Existing runtime/instance-settings/broker optional checks require coverage, not gratuitous edits.
|
||||
4. Static diff checks. Push untested candidate only as expressly ordered by sage-3836, then ask Sage for CLEAR. No tests before CLEAR.
|
||||
5. On CLEAR, new test files whole, applicable existing whole files/suites, then a mutant removing loader presence check. Restore and check source bytes. Record outputs; full landing gate is Sage's. Duckworth review follows.
|
||||
|
||||
## Pre-existing test boundary
|
||||
|
||||
Inspection finds packages/business/tests/business.test.mjs:98 loops over [human,arbiters,projects,tracker,roles] and :99 asserts deletion of each refuses. Q247 directly invalidates the tracker iteration. Proposed sole old-test edit: remove just the tracker string from that required-key list, leaving all other iterations/assertions intact, and add explicit tracker-omission coverage in a new test file. This requires Mos OK under mos-1272; stop implementation and report before editing existing tests. No tests have run.
|
||||
|
||||
|
||||
## Implementation draft and old-test ruling
|
||||
|
||||
Sage-3840 relays Mos mos-1556: mos-1272 OK to remove only tracker from business.test.mjs's required-key list, preserving the four other keys and the assertion. Done exactly, one addition/one deletion at original line98; no other old test edited. All positive/negative coverage is in two new files: business/tests/optional-tracker.test.mjs and cli/tests/optional-tracker.test.mjs. The latter covers Console businessView as required by sage-3840.
|
||||
|
||||
Design now implemented in the four source files identified above; README explains omission versus literal null. New tests cover loader/CLI absence, present value preservation, malformed/present undefined or null refusal, unchanged role-bot requirement, stale business/project tracker variables, actual runtime startup with no credential files or @sync reference, Console view, and continued missing-token refusal for a present tracker. Runtime test uses useFile on unknown @sync to distinguish an unregistered reference from a registered but unavailable credential; callback must never run. Fixture project tracker.project is set, so removing trackerFor's presence guard would actually configure sync instead of passing accidentally through the no-project guard.
|
||||
|
||||
No tests run. New node_modules symlink points to canonical checkout dependencies and is ignored; it will not be committed. Sage-3836 expressly requests push before any tests; that specific sequencing overrides the usual green-before-commit default only for this draft push. Status is untested until CLEAR. Claim wrote goldie-0324-start-1 revision3759, briefed→in-progress. Row323 remains paused at the product boundary until324 lands.
|
||||
|
||||
Static verification: node --check passed for all four edited source modules and both new test files; git diff --check passed. No node --test or shell suite was run. reader-audit.txt captures the requested grep on the draft. MANIFEST.sha256 pins README, four source files, the one existing test and both new tests. mutant.py prepares the required omission-presence mutant and restores/checks bytes in finally; it has not run.
|
||||
|
||||
Proposed CLEAR run order: new business optional-tracker.test.mjs whole (5 tests), new cli optional-tracker.test.mjs whole (4 tests), existing business.test.mjs whole, then the targeted presence mutant and restored new tests. Broaden to business/CLI/Console/bus package gates only when Sage assigns that scope/slot. All test runs use timeout -k 10 and an explicitly allocated TMPDIR under /mnt/storage/scratch/tmp.
|
||||
@@ -0,0 +1,8 @@
|
||||
ecd63bcbeae04236c445e74493d425e4a5d1bfb723d72fb58446e1d020e4d429 packages/business/README.md
|
||||
e76db42158f396ec6a44f97320355d6c2254dccb5fc53cd04e8ac42843b0d60a packages/business/src/business.mjs
|
||||
afef5c581beb26d0b0797faeb2acd3597597d48c0ce03cf31cce8d9e9b2690b9 packages/business/src/cli.mjs
|
||||
2ec0c80361f12c0d90669a3c149dfc5c9607fc9dbad98ccd30d24cf14c0de9b6 packages/cli/src/config.mjs
|
||||
46defeb3b4be0e1a22e4b89445872ad6b15cdee182076e9e6222219ae29997f4 packages/webui/src/reads.mjs
|
||||
3f99c2d7e41b37f82c3f9edeac3bc795f982dac9a3728f7694cfd5668e944691 packages/business/tests/business.test.mjs
|
||||
e92a976bcd6bf97c1cf7832d9e3fe4dec350d92809f26f7b43b938bad7e5dc49 packages/business/tests/optional-tracker.test.mjs
|
||||
7723e299fb9a11cf313d3b26df8c0270b15982731dfc0af2463c96f23e1582a2 packages/cli/tests/optional-tracker.test.mjs
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Q247 targeted presence-check mutant; run only in an approved test slot."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[4]
|
||||
source = root / 'packages/business/src/business.mjs'
|
||||
fixed = source.read_bytes()
|
||||
needle = b'Object.hasOwn(document, "tracker") ? checkTracker(document.tracker, file) : null'
|
||||
assert fixed.count(needle) == 1
|
||||
mutant = fixed.replace(needle, b'checkTracker(document.tracker, file)')
|
||||
try:
|
||||
source.write_bytes(mutant)
|
||||
result = subprocess.run([
|
||||
'timeout', '-k', '10', '90', 'node', '--test', '--test-reporter=tap',
|
||||
'--test-name-pattern=Q247: omitting tracker',
|
||||
'packages/business/tests/optional-tracker.test.mjs',
|
||||
], cwd=root, env=os.environ, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
Path(__file__).with_suffix('.log').write_text(result.stdout)
|
||||
print(f'presence mutant: exit {result.returncode}', flush=True)
|
||||
assert result.returncode == 1, result.stdout
|
||||
assert '# fail 1\n' in result.stdout and 'tracker' in result.stdout, result.stdout
|
||||
finally:
|
||||
source.write_bytes(fixed)
|
||||
assert source.read_bytes() == fixed
|
||||
print('restored sha256 ' + hashlib.sha256(fixed).hexdigest(), flush=True)
|
||||
@@ -0,0 +1,62 @@
|
||||
packages/bus/src/broker.mjs:3969: .filter(([, r]) => r.tracker?.botId !== undefined)
|
||||
packages/bus/src/broker.mjs:3970: .map(([role, r]) => [r.tracker.botId, role]),
|
||||
packages/bus/src/broker.mjs:4497: return { transaction: out.transaction, retry: false, ...(out.tracker ? { bot: out.tracker.bot } : {}) };
|
||||
packages/bus/src/broker.mjs:6242: bot = sync ? b.tracker?.sync?.bot : service === 'vikunja' ? b.roles?.[role]?.tracker?.bot : null;
|
||||
packages/bus/src/runtime.mjs:131: if (b.tracker?.sync?.credentials) references[business + '/@sync'] = b.tracker.sync.credentials;
|
||||
packages/bus/src/runtime.mjs:243: tracker: r?.tracker ?? null,
|
||||
packages/bus/src/runtime.mjs:247: sync: canonicalJson(b.tracker?.sync ?? null),
|
||||
packages/business/src/business.mjs:89: requireObject(tracker.sync, `${where}.sync`);
|
||||
packages/business/src/business.mjs:90: rejectUnknownKeys(tracker.sync, ["bot", "botId", "credentials"], `${where}.sync`);
|
||||
packages/business/src/business.mjs:92: ...checkBot({ bot: tracker.sync.bot, botId: tracker.sync.botId }, `${where}.sync`),
|
||||
packages/business/src/business.mjs:93: credentials: checkCredentialMap(tracker.sync.credentials, ["vikunja"], `${where}.sync.credentials`),
|
||||
packages/business/src/business.mjs:143: if (entry.tracker === undefined) refuse(`${at} needs "tracker" with its Vikunja bot, because ${entry.definition} uses vikunja`);
|
||||
packages/business/src/business.mjs:144: tracker = checkBot(entry.tracker, `${at}.tracker`);
|
||||
packages/business/src/business.mjs:145: if (bots.has(tracker.bot)) refuse(`${at}.tracker.bot ${tracker.bot} is used by another instance`);
|
||||
packages/business/src/business.mjs:146: if (botIds.has(tracker.botId)) refuse(`${at}.tracker.botId ${tracker.botId} is used by another instance`);
|
||||
packages/business/src/business.mjs:149: } else if (entry.tracker !== undefined) {
|
||||
packages/business/src/business.mjs:296: const tracker = Object.hasOwn(document, "tracker") ? checkTracker(document.tracker, file) : null;
|
||||
packages/business/src/business.mjs:298: if (tracker && role.tracker && (role.tracker.bot === tracker.sync.bot || role.tracker.botId === tracker.sync.botId)) {
|
||||
packages/business/src/business.mjs:299: refuse(`${file} tracker.sync must use its own bot, not the one roles.${instance} uses`);
|
||||
packages/business/src/cli.mjs:85: ...Object.values(business.tracker?.sync?.credentials ?? {}),
|
||||
packages/business/src/instance-settings.mjs:108:// needs a bus restart, then row 199's issue. A vikunja definition needs args.tracker, an existing
|
||||
packages/business/src/instance-settings.mjs:120: tracker = checkBot(args.tracker, "tracker");
|
||||
packages/business/src/instance-settings.mjs:126: const taken = Object.entries(old.roles).filter(([name, entry]) => name !== instance && entry.tracker).map(([name, entry]) => [name, entry.tracker]);
|
||||
packages/business/src/instance-settings.mjs:127: if (old.tracker?.sync) taken.push(["tracker.sync", old.tracker.sync]);
|
||||
packages/business/src/instance-settings.mjs:133: out.tracker = tracker;
|
||||
packages/business/src/instance-settings.mjs:223: const bot = old.roles[instance]?.tracker?.bot;
|
||||
packages/business/src/launch-settings.mjs:161: definition: entry.definition, vars: entry.vars ?? {}, tracker: entry.tracker ?? null,
|
||||
packages/business/src/launch-settings.mjs:235: if (entry.tracker === null) delete next.roles[instance].tracker;
|
||||
packages/business/src/launch-settings.mjs:236: else if (Object.hasOwn(entry, "tracker")) next.roles[instance].tracker = entry.tracker;
|
||||
packages/business/src/resolve.mjs:93: tracker: entry.tracker,
|
||||
packages/cli/src/config.mjs:58: if (!b.tracker) return null;
|
||||
packages/cli/src/instances.mjs:118: if (bot) args.tracker = { bot: values["--bot"], botId: Number(values["--bot-id"]) };
|
||||
packages/conversation/src/controller.mjs:443: exec.tracker = new Tracker({
|
||||
packages/conversation/src/controller.mjs:502: if (exec.link.poisoned || exec.tracker.overlapped || this.closers.has("uncertain")) return;
|
||||
packages/conversation/src/controller.mjs:748: return { fence: true, closer, fenceIdx: this.exec?.tracker.idx ?? 0 };
|
||||
packages/conversation/src/controller.mjs:807: seam: { replay: "unavailable", streamEpoch: exec?.streamEpoch ?? null, fromSequence: (exec?.seq ?? 0) + 1, reconcile: true, quiet: !exec?.tracker.current && !exec?.tracker.slot },
|
||||
packages/conversation/src/controller.mjs:820: if (exec?.tracker.slot) return refused(BUSY);
|
||||
packages/conversation/src/controller.mjs:831: exec.tracker.attach(slot);
|
||||
packages/conversation/src/controller.mjs:888: const tr = slot.exec.tracker;
|
||||
packages/conversation/src/controller.mjs:935: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:961: const exec = slot.exec, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:997: this.#settleFailure(exec.tracker.slot, RUN_OVERLAP);
|
||||
packages/conversation/src/controller.mjs:1008: this.evidence.gaps.push({ kind, detail, idx: exec.tracker.idx });
|
||||
packages/conversation/src/controller.mjs:1021: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1048: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1056: const tr = exec.tracker, s = tr.slot;
|
||||
packages/conversation/src/controller.mjs:1268: const exec = this.exec, tr = exec?.tracker;
|
||||
packages/conversation/src/controller.mjs:1308: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1330: exec.tracker.clearWindow = Math.max(0, exec.tracker.clearWindow - 1);
|
||||
packages/conversation/src/controller.mjs:1334: const { exec, stop } = ctx, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1413: const { exec } = ctx, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1447: const { exec, stop } = ctx, tr = exec.tracker;
|
||||
packages/tasks/src/adapter.mjs:25: if (!positive(b.tracker?.sync?.botId)) throw new BusError('tracker-config');
|
||||
packages/tasks/src/adapter.mjs:28: if (r.tracker) {
|
||||
packages/tasks/src/adapter.mjs:29: if (!positive(r.tracker.botId) || typeof r.definition !== 'string') throw new BusError('tracker-config');
|
||||
packages/tasks/src/adapter.mjs:30: roles[role] = { definition: r.definition, botId: r.tracker.botId };
|
||||
packages/tasks/src/adapter.mjs:35: for (const [title, id] of Object.entries(b.tracker.labels ?? {})) {
|
||||
packages/tasks/src/adapter.mjs:45: if (b.tracker?.sync?.credentials?.vikunja?.file) out['@sync'] = b.tracker.sync.credentials.vikunja.file;
|
||||
packages/tasks/src/adapter.mjs:81: bots: new Set([businesses[name].tracker.sync.botId, ...Object.values(s.roles).map((r) => r.botId)]),
|
||||
packages/webui/src/public/bus.js:785: if(f==='tracker'&&text.trim()===''){patch.tracker=null;continue;}
|
||||
packages/webui/src/reads.mjs:164: for (const [service, ref] of Object.entries(r.credentials)) credentials.push(credential(service, ref, service === 'vikunja' ? r.tracker?.bot ?? null : null, instance, now));
|
||||
packages/webui/src/reads.mjs:166: for (const [service, ref] of Object.entries(b.tracker?.sync?.credentials ?? {})) credentials.push(credential(service, ref, b.tracker.sync.bot, 'tracker sync', now));
|
||||
@@ -1603,3 +1603,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T19:29:00Z | Boyd (48a3ae38) | Row 298 (#1804) targeted T1 lane B | Five whole files 29/0/0, ten mutants killed, source restored; reviewer delta prepared. Earlier startup timestamp corrected in Boyd BUILD.md. Full gate and queue/landing remain Sage's.
|
||||
|
||||
2026-10-23T22:41:07Z | Boyd (48a3ae38) | Row 298 rebase under sage-3794; row 321 review assigned | Rebased candidate on boyd/row298-r2 at base833ec1cc8; append-only log conflicts resolved preserving upstream, writer-lock revoke retained; no Node/tests before CLEAR. Row321 awaits pushed candidate/review request.
|
||||
2026-10-23T23:15Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 324 / #1820 optional tracker design | New goldie/row324 worktree from approved860afc2b6, pinned brief f375f4e92, complete reader audit/plan. Existing required-key test contradicts Q247; requesting Mos exception before implementation. Tests held until CLEAR; row323 remains deferred until324 lands.
|
||||
2026-10-23T23:18Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 324 / #1820 untested draft | Claim3759; optional tracker implementation, new loader/CLI/boot/Console tests, complete reader audit and prepared mutant. Mos-1556 permits sole existing required-key-list change. Syntax/diff checks pass; no tests before CLEAR. Untested push authorized sage-3836.
|
||||
@@ -81,6 +81,13 @@ by group or other. The loader checks the owner and mode on the descriptor
|
||||
it reads from, so the file can't be swapped between the check and the
|
||||
read.
|
||||
|
||||
The business-level `tracker` is optional (Q247). Omit it for a business with
|
||||
no tracker sync: validate and bus boot need no sync credential, and the
|
||||
Console shows no sync credential entry. The loader represents omission as
|
||||
`tracker: null`; writing a literal `null` in the business file still refuses.
|
||||
A present tracker keeps its existing bot, credential and validation rules.
|
||||
Tracker variables alone do not enable sync when the tracker block is absent.
|
||||
|
||||
Each role instance names a `definition` (a version 2 role file), an
|
||||
optional `holder`, its Vikunja bot (`tracker`, required when the definition
|
||||
uses Vikunja), a credential reference for each service the definition
|
||||
|
||||
@@ -236,7 +236,7 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
|
||||
requireId(document.id, "business id");
|
||||
const base = file.split("/").pop().replace(/\.json$/, "");
|
||||
if (document.id !== base) refuse(`business "id" (${document.id}) must match its filename (${base}.json)`);
|
||||
for (const key of ["human", "arbiters", "projects", "tracker", "roles"]) {
|
||||
for (const key of ["human", "arbiters", "projects", "roles"]) {
|
||||
if (document[key] === undefined) refuse(`business file requires "${key}" (${file})`);
|
||||
}
|
||||
const human = requireId(document.human, "business human");
|
||||
@@ -293,9 +293,9 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
|
||||
}
|
||||
}
|
||||
|
||||
const tracker = checkTracker(document.tracker, file);
|
||||
const tracker = Object.hasOwn(document, "tracker") ? checkTracker(document.tracker, file) : null;
|
||||
for (const [instance, role] of Object.entries(roles)) {
|
||||
if (role.tracker && (role.tracker.bot === tracker.sync.bot || role.tracker.botId === tracker.sync.botId)) {
|
||||
if (tracker && role.tracker && (role.tracker.bot === tracker.sync.bot || role.tracker.botId === tracker.sync.botId)) {
|
||||
refuse(`${file} tracker.sync must use its own bot, not the one roles.${instance} uses`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ function validate(args) {
|
||||
projects[id] = "valid";
|
||||
}
|
||||
const refs = [
|
||||
...Object.values(business.tracker.sync.credentials),
|
||||
...Object.values(business.tracker?.sync?.credentials ?? {}),
|
||||
...Object.values(business.roles).flatMap((r) => Object.values(r.credentials)),
|
||||
];
|
||||
const checked = checkRefs(refs, forbiddenRoots(config, business));
|
||||
|
||||
@@ -95,7 +95,7 @@ test("top-level refusals", () => {
|
||||
refuses(() => check((d) => { d.owner = "x"; }), /unsupported business file key: "owner"/);
|
||||
refuses(() => check((d) => { d.id = "other"; }), /must match its filename/);
|
||||
refuses(() => check((d) => { d.id = "Acme"; }), /business id/);
|
||||
for (const key of ["human", "arbiters", "projects", "tracker", "roles"]) {
|
||||
for (const key of ["human", "arbiters", "projects", "roles"]) {
|
||||
refuses(() => check((d) => { delete d[key]; }), new RegExp(`requires "${key}"`));
|
||||
}
|
||||
refuses(() => check((d) => { d.vars.harness = "pi"; }), /harness can't be set at the business layer/);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Q247 / row 324 (#1820): optional business tracker; role credential rules remain exact.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { loadBusiness, validateBusinessDocument } from "../src/index.mjs";
|
||||
import { businessDoc, REPO, REPO_ROLES, systemConfig, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
function fixture(t) {
|
||||
const root = tmp();
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const doc = businessDoc(root);
|
||||
const config = systemConfig(root);
|
||||
const file = join(root, "config", "businesses", "acme.json");
|
||||
return { root, doc, config, file };
|
||||
}
|
||||
|
||||
const check = (x) => validateBusinessDocument(x.doc, x.file, { rolesDir: REPO_ROLES });
|
||||
|
||||
test("Q247: omitting tracker loads as null and preserves independent role bots", (t) => {
|
||||
const x = fixture(t);
|
||||
delete x.doc.tracker;
|
||||
writeJson(x.file, x.doc);
|
||||
const b = loadBusiness("acme", { dir: join(x.root, "config"), rolesDir: REPO_ROLES });
|
||||
assert.equal(b.tracker, null);
|
||||
assert.equal(b.roles.pm.tracker.bot, x.doc.roles.pm.tracker.bot);
|
||||
assert.equal(b.roles.pm.credentials.vikunja.file, x.doc.roles.pm.credentials.vikunja.file);
|
||||
assert.equal(Object.hasOwn(x.doc, "tracker"), false);
|
||||
});
|
||||
|
||||
test("Q247: real business validate passes without a sync credential reference or file", (t) => {
|
||||
const x = fixture(t);
|
||||
rmSync(x.doc.tracker.sync.credentials.vikunja.file);
|
||||
delete x.doc.tracker;
|
||||
writeJson(x.file, x.doc);
|
||||
const r = spawnSync(process.execPath, [join(REPO, "packages/business/src/cli.mjs"), "validate", "acme"], {
|
||||
env: { ...process.env, HOME: x.root, MOSAIC_CONFIG: x.config, MOSAIC_ROLES_DIR: REPO_ROLES },
|
||||
encoding: "utf8", timeout: 30000,
|
||||
});
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
assert.equal(JSON.parse(r.stdout).business, "acme");
|
||||
assert.doesNotMatch(r.stderr, /credential reference problem|not found/);
|
||||
});
|
||||
|
||||
test("Q247: present tracker retains its values and validation without mutating the input", (t) => {
|
||||
const x = fixture(t);
|
||||
const before = structuredClone(x.doc);
|
||||
const b = check(x);
|
||||
assert.equal(b.tracker.sync.bot, x.doc.tracker.sync.bot);
|
||||
assert.equal(b.tracker.sync.botId, x.doc.tracker.sync.botId);
|
||||
assert.equal(b.tracker.sync.credentials.vikunja.file, x.doc.tracker.sync.credentials.vikunja.file);
|
||||
assert.deepEqual(b.tracker.labels, {});
|
||||
assert.deepEqual(x.doc, before);
|
||||
assert.ok(Object.isFrozen(b.tracker.sync));
|
||||
});
|
||||
|
||||
test("Q247: present null, undefined and malformed trackers still refuse", (t) => {
|
||||
const x = fixture(t);
|
||||
for (const tracker of [null, undefined, false, [], {}, { sync: {} }]) {
|
||||
x.doc.tracker = tracker;
|
||||
assert.throws(() => check(x), /tracker/, String(tracker));
|
||||
}
|
||||
});
|
||||
|
||||
test("Q247: absent business tracker does not waive a role's Vikunja bot requirement", (t) => {
|
||||
const x = fixture(t);
|
||||
delete x.doc.tracker;
|
||||
delete x.doc.roles.pm.tracker;
|
||||
assert.throws(() => check(x), /roles\.pm needs "tracker"/);
|
||||
});
|
||||
@@ -55,6 +55,7 @@ function business(fn) {
|
||||
// and no task verbs; two projects naming one each refuse, since the boot
|
||||
// shape holds one tracker project per business.
|
||||
function trackerFor(system, b, warn) {
|
||||
if (!b.tracker) return null;
|
||||
const first = Object.keys(b.roles)[0];
|
||||
const base = resolveInstance({ system, business: b, instance: first }).vars;
|
||||
if (base["tracker.baseUrl"] === undefined) return null;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Q247 / row 324 (#1820): no tracker adapter, credential registration or Console sync row.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { bootConfig } from "../src/config.mjs";
|
||||
import { startBroker } from "../../bus/src/runtime.mjs";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { loadBusiness } from "../../business/src/index.mjs";
|
||||
import { businessDoc, launchDoc, REPO_ROLES, systemConfig, tmp, writeJson } from "../../business/tests/helpers.mjs";
|
||||
import { businessView } from "../../webui/src/reads.mjs";
|
||||
|
||||
function fixture(t, { present = false } = {}) {
|
||||
const root = tmp();
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const doc = businessDoc(root);
|
||||
// Credential-free roles, so an empty runtime credential list proves no phantom @sync entry.
|
||||
doc.roles = {
|
||||
chief: { definition: "chief-of-staff", vars: { harness: "claude-code", model: "claude-opus-5-5", thinking: "high" } },
|
||||
assistant: { definition: "assistant", vars: { harness: "claude-code", model: "claude-opus-5-5", thinking: "high" } },
|
||||
};
|
||||
doc.arbiters = { delivery: "chief", technical: "chief" };
|
||||
doc.launch = launchDoc("chief", ["assistant"]);
|
||||
doc.plugins = { harness: ["claude-code"] };
|
||||
// Deliberately retain fixture tracker.baseUrl; missing business tracker must still mean no sync.
|
||||
if (!present) delete doc.tracker;
|
||||
// No credential files remain, so boot cannot accidentally rely on the old fixture's sync token.
|
||||
rmSync(join(root, "secrets"), { recursive: true });
|
||||
writeJson(join(root, "project/.mosaic/project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 3 } });
|
||||
const config = systemConfig(root);
|
||||
const system = JSON.parse(readFileSync(config, "utf8"));
|
||||
mkdirSync(system.dataRoot, { mode: 0o700 });
|
||||
writeJson(join(root, "config/businesses/acme.json"), doc);
|
||||
const env = { HOME: root, MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: REPO_ROLES };
|
||||
return { root, doc, system, env, b: loadBusiness("acme", { dir: join(root, "config"), rolesDir: REPO_ROLES }) };
|
||||
}
|
||||
|
||||
test("Q247: bootConfig omits tracker setup even with tracker variables left in the business", (t) => {
|
||||
const x = fixture(t);
|
||||
assert.ok(x.doc.vars["tracker.baseUrl"]);
|
||||
const boot = bootConfig({ system: x.system, businessId: "acme", env: x.env });
|
||||
assert.equal(boot.businesses.acme.tracker, null);
|
||||
assert.equal(Object.hasOwn(boot, "trackers"), false);
|
||||
});
|
||||
|
||||
test("Q247: trackerless business boots and registers no @sync credential", async (t) => {
|
||||
const x = fixture(t);
|
||||
const boot = bootConfig({ system: x.system, businessId: "acme", env: x.env });
|
||||
const runtime = await startBroker(boot);
|
||||
t.after(() => runtime.close());
|
||||
assert.deepEqual(runtime.credentials.status(), []);
|
||||
await assert.rejects(runtime.credentials.useFile("acme/@sync", "vikunja", () => assert.fail("no sync token may be used")),
|
||||
(e) => e.code === "credential-unknown");
|
||||
const reader = new Client({ path: runtime.path, cap: runtime.readers[0].cap });
|
||||
assert.deepEqual(await reader.call("credentials"), []);
|
||||
});
|
||||
|
||||
test("Q247: Console business read works with tracker absent and shows no sync credential", (t) => {
|
||||
const x = fixture(t);
|
||||
const view = businessView(x.b, { system: x.system });
|
||||
assert.equal(view.id, "acme");
|
||||
assert.deepEqual(view.credentials, []);
|
||||
assert.deepEqual(view.instances.map((entry) => entry.instance), ["chief", "assistant"]);
|
||||
});
|
||||
|
||||
|
||||
test("Q247: a present tracker with a missing token still refuses bus boot", async (t) => {
|
||||
const x = fixture(t, { present: true });
|
||||
const boot = bootConfig({ system: x.system, businessId: "acme", env: x.env });
|
||||
assert.equal(boot.businesses.acme.tracker.sync.bot, x.doc.tracker.sync.bot);
|
||||
await assert.rejects(startBroker(boot), (e) => e.code === "credential-file");
|
||||
});
|
||||
@@ -163,7 +163,7 @@ export function businessView(b, { system = null, now = Date.now() } = {}) {
|
||||
for (const [instance, r] of Object.entries(b.roles)) {
|
||||
for (const [service, ref] of Object.entries(r.credentials)) credentials.push(credential(service, ref, service === 'vikunja' ? r.tracker?.bot ?? null : null, instance, now));
|
||||
}
|
||||
for (const [service, ref] of Object.entries(b.tracker.sync.credentials)) credentials.push(credential(service, ref, b.tracker.sync.bot, 'tracker sync', now));
|
||||
for (const [service, ref] of Object.entries(b.tracker?.sync?.credentials ?? {})) credentials.push(credential(service, ref, b.tracker.sync.bot, 'tracker sync', now));
|
||||
return {
|
||||
id: b.id, human: b.human, arbiters: b.arbiters, projects: Object.keys(b.projects), actions: [...ACTIONS],
|
||||
vars: pick(b.vars, VAR_KEYS), instances, launch: b.launch, credentials,
|
||||
|
||||
Reference in new issue
Block a user