feat(ms-ai-architect): C3.1 — keychain + Learn Platform API-klient (TDD) [skip-docs]
C3 kurs-deteksjon, fase C3.1 (spec docs/c3-course-detection-plan.md §6/§7). Claude-frie, fil-frie infrastruktur-lag for kurs-detektoren (C3.4): - lib/keychain.mjs: readSecret() leser én Keychain-item via `security`-CLI, fail-soft (manglende item / ikke-macOS → null). execImpl-DI for test. - lib/learn-api.mjs: getToken (Entra client-credentials), paginate (async-gen, følger nextLink), buildApiUrl (pinner api-version), buildUpdatedAtGt (kaster på dato-only, canon Z — gotcha #1). Robusthetskontrakt §6: response.ok-sjekk (fetch kaster ikke på 4xx/5xx), AbortSignal.timeout, 429/5xx-retry som respekterer Retry-After, 4xx≠429 ikke-retry, fail-closed. fetchImpl/sleep-DI. Tester (24, alle grønne): test-learn-api (token-body, buildUpdatedAtGt, nextLink-paginering, produkt-param, alle robusthets-asserts), test-keychain, test-courses-invariant (Claude-fri + skriver-ingen-filer). Live-probe (efemer) grønn mot ekte Platform API m/ ekte creds: token + full-enum + inkrementell. Intern infrastruktur (ingen brukervendt kommando/hook/atferd endret ennå — surfacing + docs lander i C3.6). kb-update 213→237 · validate 239 · null regresjon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
215437cb5d
commit
0390cc10ca
5 changed files with 488 additions and 0 deletions
38
tests/kb-update/test-keychain.test.mjs
Normal file
38
tests/kb-update/test-keychain.test.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// tests/kb-update/test-keychain.test.mjs
|
||||
// Unit tests for scripts/kb-update/lib/keychain.mjs (C3.1).
|
||||
// readSecret reads a single Keychain item via the `security` CLI. Tests inject a
|
||||
// stub exec impl so no real Keychain / no real secret ever touches the suite
|
||||
// (spec §3: "Tester injiserer falske creds via en stub").
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readSecret } from '../../scripts/kb-update/lib/keychain.mjs';
|
||||
|
||||
test('readSecret — returns the trimmed secret via injected exec', () => {
|
||||
const calls = [];
|
||||
const execImpl = (cmd, args, opts) => {
|
||||
calls.push({ cmd, args, opts });
|
||||
return 'secret-value\n';
|
||||
};
|
||||
const v = readSecret('learn-platform-tenant-id', 'ktg', { execImpl });
|
||||
assert.equal(v, 'secret-value');
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].cmd, 'security');
|
||||
assert.deepEqual(calls[0].args, [
|
||||
'find-generic-password', '-s', 'learn-platform-tenant-id', '-a', 'ktg', '-w',
|
||||
]);
|
||||
assert.equal(calls[0].opts.encoding, 'utf8');
|
||||
});
|
||||
|
||||
test('readSecret — returns null when the item is missing (exec throws)', () => {
|
||||
const execImpl = () => { throw new Error('SecKeychain item not found'); };
|
||||
assert.equal(readSecret('does-not-exist', 'ktg', { execImpl }), null);
|
||||
});
|
||||
|
||||
test('readSecret — defaults account to ktg', () => {
|
||||
let captured;
|
||||
const execImpl = (_cmd, args) => { captured = args; return 'x'; };
|
||||
readSecret('svc', undefined, { execImpl });
|
||||
assert.ok(captured.includes('-a'));
|
||||
assert.equal(captured[captured.indexOf('-a') + 1], 'ktg');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue